bc2633a2086a99231717285ec47d819dd35a7c1d
[srvgit] / t / db_dependent / Holds / DisallowHoldIfItemsAvailable.t
1 #!/usr/bin/perl
2
3 use Modern::Perl;
4
5 use C4::Context;
6 use C4::Circulation;
7 use C4::Items;
8 use Koha::Items;
9 use Koha::CirculationRules;
10
11 use Test::More tests => 13;
12
13 use t::lib::TestBuilder;
14 use t::lib::Mocks;
15
16 BEGIN {
17     use_ok('C4::Reserves');
18 }
19
20 my $schema = Koha::Database->schema;
21 $schema->storage->txn_begin;
22 my $dbh = C4::Context->dbh;
23
24 my $builder = t::lib::TestBuilder->new;
25
26 my $library1 = $builder->build( { source => 'Branch', } );
27 my $library2 = $builder->build( { source => 'Branch', } );
28 my $itemtype = $builder->build(
29     {   source => 'Itemtype',
30         value  => { notforloan => 0 }
31     }
32 )->{itemtype};
33
34 t::lib::Mocks::mock_userenv( { branchcode => $library1->{branchcode} } );
35
36 my $patron1 = $builder->build_object(
37     {   class => 'Koha::Patrons',
38         value => {
39             branchcode => $library1->{branchcode},
40             dateexpiry => '3000-01-01',
41         }
42     }
43 );
44 my $borrower1 = $patron1->unblessed;
45
46 my $patron2 = $builder->build_object(
47     {   class => 'Koha::Patrons',
48         value => {
49             branchcode => $library1->{branchcode},
50             dateexpiry => '3000-01-01',
51         }
52     }
53 );
54
55 my $patron3 = $builder->build_object(
56     {   class => 'Koha::Patrons',
57         value => {
58             branchcode => $library2->{branchcode},
59             dateexpiry => '3000-01-01',
60         }
61     }
62 );
63
64 my $library_A = $library1->{branchcode};
65 my $library_B = $library2->{branchcode};
66
67 my $biblio       = $builder->build_sample_biblio( { itemtype => $itemtype } );
68 my $biblionumber = $biblio->biblionumber;
69 my $item1        = $builder->build_sample_item(
70     {   biblionumber  => $biblionumber,
71         itype         => $itemtype,
72         homebranch    => $library_A,
73         holdingbranch => $library_A
74     }
75 );
76 my $item2 = $builder->build_sample_item(
77     {   biblionumber  => $biblionumber,
78         itype         => $itemtype,
79         homebranch    => $library_A,
80         holdingbranch => $library_A
81     }
82 );
83
84 # Test hold_fulfillment_policy
85 $dbh->do("DELETE FROM circulation_rules");
86 Koha::CirculationRules->set_rules(
87     {   categorycode => undef,
88         itemtype     => $itemtype,
89         branchcode   => undef,
90         rules        => {
91             issuelength     => 7,
92             lengthunit      => 8,
93             reservesallowed => 99,
94             onshelfholds    => 2,
95         }
96     }
97 );
98
99 my $is;
100
101 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );
102 is( $is, 1, "Items availability: both of 2 items are available" );
103
104 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
105 is( $is, 0, "Item cannot be held, 2 items available" );
106
107 my $issue1 = AddIssue( $patron2->unblessed, $item1->barcode );
108
109 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );
110 is( $is, 1, "Items availability: one item is available" );
111
112 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
113 is( $is, 0, "Item cannot be held, 1 item available" );
114
115 AddIssue( $patron2->unblessed, $item2->barcode );
116
117 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );
118 is( $is, 0, "Items availability: none of items are available" );
119
120 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
121 is( $is, 1, "Item can be held, no items available" );
122
123 AddReturn( $item1->barcode );
124
125 {    # Remove the issue for the first patron, and modify the branch for item1
126     subtest 'IsAvailableForItemLevelRequest behaviours depending on ReservesControlBranch + holdallowed' => sub {
127         plan tests => 2;
128
129         my $hold_allowed_from_home_library  = 'from_home_library';
130         my $hold_allowed_from_any_libraries = 'from_any_library';
131
132         subtest 'Item is available at a different library' => sub {
133             plan tests => 13;
134
135             $item1->set( { homebranch => $library_B, holdingbranch => $library_B } )->store;
136
137             #Scenario is:
138             #One shelf holds is 'If all unavailable'/2
139             #Item 1 homebranch library B is available
140             #Item 2 homebranch library A is checked out
141             #Borrower1 is from library A
142
143             {
144                 set_holdallowed_rule($hold_allowed_from_home_library);
145
146                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
147
148                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 0 items, library B 1 item
149                 is( $is, 0, "Items availability: hold allowed from home + ReservesControlBranch=ItemHomeLibrary + one item is available at different library" );
150
151                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
152                 is( $is, 1,
153                         "Hold allowed from home library + ReservesControlBranch=ItemHomeLibrary, "
154                       . "One item is available at different library, not holdable = none available => the hold is allowed at item level" );
155                 $is = IsAvailableForItemLevelRequest( $item1, $patron2 );
156                 is( $is, 1,
157                         "Hold allowed from home library + ReservesControlBranch=ItemHomeLibrary, "
158                       . "One item is available at home library, holdable = one available => the hold is not allowed at item level" );
159                 set_holdallowed_rule( $hold_allowed_from_any_libraries, $library_B );
160
161                 #Adding a rule for the item's home library affects the availability for a borrower from another library because ReservesControlBranch is set to ItemHomeLibrary
162
163                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 0 items, library B 1 item
164                 is( $is, 1,
165                     "Items availability: hold allowed from any library for library B + ReservesControlBranch=ItemHomeLibrary + one item is available at different library" );
166
167                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
168                 is( $is, 0,
169                         "Hold allowed from home library + ReservesControlBranch=ItemHomeLibrary, "
170                       . "One item is available at different library, holdable = one available => the hold is not allowed at item level" );
171
172                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
173
174                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 0 items, library B 1 item
175                 is( $is, 0,
176                     "Items availability: hold allowed from any library for library B + ReservesControlBranch=PatronLibrary + one item is available at different library" );
177
178                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
179                 is( $is, 1,
180                         "Hold allowed from home library + ReservesControlBranch=PatronLibrary, "
181                       . "One item is available at different library, not holdable = none available => the hold is allowed at item level" );
182
183                 #Adding a rule for the patron's home library affects the availability for an item from another library because ReservesControlBranch is set to PatronLibrary
184                 set_holdallowed_rule( $hold_allowed_from_any_libraries, $library_A );
185
186                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 0 items, library B 1 item
187                 is( $is, 1,
188                     "Items availability: hold allowed from any library for library A + ReservesControlBranch=PatronLibrary + one item is available at different library" );
189
190                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
191                 is( $is, 0,
192                         "Hold allowed from home library + ReservesControlBranch=PatronLibrary, "
193                       . "One item is available at different library, holdable = one available => the hold is not allowed at item level" );
194             }
195
196             {
197                 set_holdallowed_rule($hold_allowed_from_any_libraries);
198
199                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
200
201                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 0 items, library B 1 item
202                 is( $is, 1, "Items availability: hold allowed from any library + ReservesControlBranch=ItemHomeLibrary + one item is available at different library" );
203
204                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
205                 is( $is, 0,
206                         "Hold allowed from any library + ReservesControlBranch=ItemHomeLibrary, "
207                       . "One item is available at the diff library, holdable = 1 available => the hold is not allowed at item level" );
208
209                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
210
211                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 0 items, library B 1 item
212                 is( $is, 1, "Items availability: hold allowed from any library + ReservesControlBranch=PatronLibrary + one item is available at different library" );
213
214                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
215                 is( $is, 0,
216                         "Hold allowed from any library + ReservesControlBranch=PatronLibrary, "
217                       . "One item is available at the diff library, holdable = 1 available => the hold is not allowed at item level" );
218             }
219         };
220
221         subtest 'Item is available at the same library' => sub {
222             plan tests => 8;
223
224             $item1->set( { homebranch => $library_A, holdingbranch => $library_A } )->store;
225
226             #Scenario is:
227             #One shelf holds is 'If all unavailable'/2
228             #Item 1 homebranch library A is available
229             #Item 2 homebranch library A is checked out
230             #Borrower1 is from library A
231             #CircControl has no effect - same rule for all branches as set at line 96
232             #ReservesControlBranch is not checked in these subs we are testing?
233
234             {
235                 set_holdallowed_rule($hold_allowed_from_home_library);
236
237                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
238
239                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 1 item
240                 is( $is, 1, "Items availability: hold allowed from home library + ReservesControlBranch=ItemHomeLibrary + one item is available at home library" );
241
242                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
243                 is( $is, 0,
244                         "Hold allowed from home library + ReservesControlBranch=ItemHomeLibrary, "
245                       . "One item is available at the same library, holdable = 1 available  => the hold is not allowed at item level" );
246
247                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
248
249                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 1 item
250                 is( $is, 1, "Items availability: hold allowed from home library + ReservesControlBranch=PatronLibrary + one item is available at home library" );
251
252                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
253                 is( $is, 0,
254                         "Hold allowed from home library + ReservesControlBranch=PatronLibrary, "
255                       . "One item is available at the same library, holdable = 1 available  => the hold is not allowed at item level" );
256             }
257
258             {
259                 set_holdallowed_rule($hold_allowed_from_any_libraries);
260
261                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
262
263                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 1 item
264                 is( $is, 1, "Items availability: hold allowed from any library + ReservesControlBranch=ItemHomeLibrary + one item is available at home library" );
265
266                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
267                 is( $is, 0,
268                         "Hold allowed from any library + ReservesControlBranch=ItemHomeLibrary, "
269                       . "One item is available at the same library, holdable = 1 available => the hold is not allowed at item level" );
270
271                 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
272
273                 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 1 item
274                 is( $is, 1, "Items availability: hold allowed from any library + ReservesControlBranch=PatronLibrary + one item is available at home library" );
275
276                 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
277                 is( $is, 0,
278                         "Hold allowed from any library + ReservesControlBranch=PatronLibrary, "
279                       . "One item is available at the same library, holdable = 1 available  => the hold is not allowed at item level" );
280             }
281         };
282     };
283 }
284
285 my $itemtype2 = $builder->build(
286     {   source => 'Itemtype',
287         value  => { notforloan => 0 }
288     }
289 )->{itemtype};
290 my $item3 = $builder->build_sample_item( { itype => $itemtype2 } );
291
292 my $hold = $builder->build(
293     {   source => 'Reserve',
294         value  => {
295             itemnumber => $item3->itemnumber,
296             found      => 'T'
297         }
298     }
299 );
300
301 Koha::CirculationRules->set_rules(
302     {   categorycode => undef,
303         itemtype     => $itemtype2,
304         branchcode   => undef,
305         rules        => {
306             maxissueqty  => 99,
307             onshelfholds => 0,
308         }
309     }
310 );
311
312 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );    # patron1 in library A, library A 1 item
313 is( $is, 1, "Items availability: 1 item is available, 1 item held in T" );
314
315 $is = IsAvailableForItemLevelRequest( $item3, $patron1 );
316 is( $is, 1, "Item can be held, items in transit are not available" );
317
318 subtest 'Check holds availability with different item types' => sub {
319     plan tests => 6;
320
321     # Check for holds availability when different item types have different
322     # smart rules assigned both with "if all unavailable" set,
323     # and $itemtype rule allows holds, $itemtype2 rule disallows holds.
324     # So, $item should be available for hold when checked out even if $item2
325     # is not checked out, because anyway $item2 unavailable for holds by rule
326     # (Bug 24683):
327
328     my $biblio2       = $builder->build_sample_biblio( { itemtype => $itemtype } );
329     my $item4         = $builder->build_sample_item(
330         {   biblionumber  => $biblio2->biblionumber,
331             itype         => $itemtype,
332             homebranch    => $library_A,
333             holdingbranch => $library_A
334         }
335     );
336     my $item5 = $builder->build_sample_item(
337         {   biblionumber  => $biblio2->biblionumber,
338             itype         => $itemtype2,
339             homebranch    => $library_A,
340             holdingbranch => $library_A
341         }
342     );
343
344     # Test hold_fulfillment_policy
345     $dbh->do("DELETE FROM circulation_rules");
346     Koha::CirculationRules->set_rules(
347         {   categorycode => undef,
348             itemtype     => $itemtype,
349             branchcode   => undef,
350             rules        => {
351                 issuelength      => 7,
352                 lengthunit       => 8,
353                 reservesallowed  => 99,
354                 holds_per_record => 99,
355                 onshelfholds     => 2,
356             }
357         }
358     );
359     Koha::CirculationRules->set_rules(
360         {   categorycode => undef,
361             itemtype     => $itemtype2,
362             branchcode   => undef,
363             rules        => {
364                 issuelength      => 7,
365                 lengthunit       => 8,
366                 reservesallowed  => 0,
367                 holds_per_record => 0,
368                 onshelfholds     => 2,
369             }
370         }
371     );
372
373     $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblio2->biblionumber, patron => $patron1 } );
374     is( $is, 1, "Items availability: 2 items, one allowed by smart rule but not checked out, another one not allowed to be held by smart rule" );
375
376     $is = IsAvailableForItemLevelRequest( $item4, $patron1 );
377     is( $is, 0, "Item4 cannot be requested to hold: 2 items, Item4 available, Item5 restricted" );
378
379     $is = IsAvailableForItemLevelRequest( $item5, $patron1 );
380     is( $is, 0, "Item5 cannot be requested to hold: 2 items, Item4 available, Item5 restricted" );
381
382     AddIssue( $patron2->unblessed, $item4->barcode );
383
384     $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblio2->biblionumber, patron => $patron1 } );
385     is( $is, 0, "Items availability: 2 items, one allowed by smart rule and checked out, another one not allowed to be held by smart rule" );
386
387     $is = IsAvailableForItemLevelRequest( $item4, $patron1 );
388     is( $is, 1, "Item4 can be requested to hold, 2 items, Item4 checked out, Item5 restricted" );
389
390     $is = IsAvailableForItemLevelRequest( $item5, $patron1 );
391     # Note: read IsAvailableForItemLevelRequest sub description about CanItemBeReserved/CanBookBeReserved:
392     is( $is, 1, "Item5 can be requested to hold, 2 items, Item4 checked out, Item5 restricted" );
393 };
394
395 subtest 'Check item checkout availability with ordered item' => sub {
396     plan tests => 1;
397
398     my $biblio2       = $builder->build_sample_biblio( { itemtype => $itemtype } );
399     my $item1 = $builder->build_sample_item(
400         {   biblionumber  => $biblio2->biblionumber,
401             itype         => $itemtype2,
402             homebranch    => $library_A,
403             holdingbranch => $library_A,
404             notforloan    => -1
405         }
406     );
407
408     $dbh->do("DELETE FROM circulation_rules");
409     Koha::CirculationRules->set_rules(
410         {   categorycode => undef,
411             itemtype     => $itemtype2,
412             branchcode   => undef,
413             rules        => {
414                 issuelength      => 7,
415                 lengthunit       => 8,
416                 reservesallowed  => 99,
417                 holds_per_record => 99,
418                 onshelfholds     => 2,
419             }
420         }
421     );
422
423     $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblio2->biblionumber, patron => $patron1 } );
424     is( $is, 0, "Ordered item cannot be checked out" );
425 };
426
427 subtest 'Check item availability for hold with ordered item' => sub {
428     plan tests => 1;
429
430     my $biblio2       = $builder->build_sample_biblio( { itemtype => $itemtype } );
431     my $item1 = $builder->build_sample_item(
432         {   biblionumber  => $biblio2->biblionumber,
433             itype         => $itemtype2,
434             homebranch    => $library_A,
435             holdingbranch => $library_A,
436             notforloan    => -1
437         }
438     );
439
440     $dbh->do("DELETE FROM circulation_rules");
441     Koha::CirculationRules->set_rules(
442         {   categorycode => undef,
443             itemtype     => $itemtype2,
444             branchcode   => undef,
445             rules        => {
446                 issuelength      => 7,
447                 lengthunit       => 8,
448                 reservesallowed  => 99,
449                 holds_per_record => 99,
450                 onshelfholds     => 2,
451             }
452         }
453     );
454
455     $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
456     is( $is, 1, "Ordered items are available for hold" );
457 };
458
459
460 # Cleanup
461 $schema->storage->txn_rollback;
462
463 sub set_holdallowed_rule {
464     my ( $holdallowed, $branchcode ) = @_;
465     Koha::CirculationRules->set_rules(
466         {   branchcode => $branchcode || undef,
467             itemtype   => undef,
468             rules      => {
469                 holdallowed             => $holdallowed,
470                 hold_fulfillment_policy => 'any',
471                 returnbranch            => 'homebranch',
472             }
473         }
474     );
475 }