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