6 use C4::Circulation qw( AddIssue AddReturn );
9 use Koha::CirculationRules;
11 use Test::More tests => 13;
13 use t::lib::TestBuilder;
17 use_ok('C4::Reserves', qw( ItemsAnyAvailableAndNotRestricted IsAvailableForItemLevelRequest ));
20 my $schema = Koha::Database->schema;
21 $schema->storage->txn_begin;
22 my $dbh = C4::Context->dbh;
24 my $builder = t::lib::TestBuilder->new;
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 }
34 t::lib::Mocks::mock_userenv( { branchcode => $library1->{branchcode} } );
36 my $patron1 = $builder->build_object(
37 { class => 'Koha::Patrons',
39 branchcode => $library1->{branchcode},
40 dateexpiry => '3000-01-01',
44 my $borrower1 = $patron1->unblessed;
46 my $patron2 = $builder->build_object(
47 { class => 'Koha::Patrons',
49 branchcode => $library1->{branchcode},
50 dateexpiry => '3000-01-01',
55 my $patron3 = $builder->build_object(
56 { class => 'Koha::Patrons',
58 branchcode => $library2->{branchcode},
59 dateexpiry => '3000-01-01',
64 my $library_A = $library1->{branchcode};
65 my $library_B = $library2->{branchcode};
67 my $biblio = $builder->build_sample_biblio( { itemtype => $itemtype } );
68 my $biblionumber = $biblio->biblionumber;
69 my $item1 = $builder->build_sample_item(
70 { biblionumber => $biblionumber,
72 homebranch => $library_A,
73 holdingbranch => $library_A
76 my $item2 = $builder->build_sample_item(
77 { biblionumber => $biblionumber,
79 homebranch => $library_A,
80 holdingbranch => $library_A
84 # Test hold_fulfillment_policy
85 $dbh->do("DELETE FROM circulation_rules");
86 Koha::CirculationRules->set_rules(
87 { categorycode => undef,
88 itemtype => $itemtype,
93 reservesallowed => 99,
101 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );
102 is( $is, 1, "Items availability: both of 2 items are available" );
104 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
105 is( $is, 0, "Item cannot be held, 2 items available" );
107 my $issue1 = AddIssue( $patron2->unblessed, $item1->barcode );
109 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );
110 is( $is, 1, "Items availability: one item is available" );
112 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
113 is( $is, 0, "Item cannot be held, 1 item available" );
115 AddIssue( $patron2->unblessed, $item2->barcode );
117 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } );
118 is( $is, 0, "Items availability: none of items are available" );
120 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
121 is( $is, 1, "Item can be held, no items available" );
123 AddReturn( $item1->barcode );
125 { # Remove the issue for the first patron, and modify the branch for item1
126 subtest 'IsAvailableForItemLevelRequest behaviours depending on ReservesControlBranch + holdallowed' => sub {
129 my $hold_allowed_from_home_library = 'from_home_library';
130 my $hold_allowed_from_any_libraries = 'from_any_library';
132 subtest 'Item is available at a different library' => sub {
135 $item1->set( { homebranch => $library_B, holdingbranch => $library_B } )->store;
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
144 set_holdallowed_rule($hold_allowed_from_home_library);
146 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
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" );
151 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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 );
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 );
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
163 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } ); # patron1 in library A, library A 0 items, library B 1 item
165 "Items availability: hold allowed from any library for library B + ReservesControlBranch=ItemHomeLibrary + one item is available at different library" );
167 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
172 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
174 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } ); # patron1 in library A, library A 0 items, library B 1 item
176 "Items availability: hold allowed from any library for library B + ReservesControlBranch=PatronLibrary + one item is available at different library" );
178 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
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 );
186 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron1 } ); # patron1 in library A, library A 0 items, library B 1 item
188 "Items availability: hold allowed from any library for library A + ReservesControlBranch=PatronLibrary + one item is available at different library" );
190 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
197 set_holdallowed_rule($hold_allowed_from_any_libraries);
199 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
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" );
204 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
209 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
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" );
214 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
221 subtest 'Item is available at the same library' => sub {
224 $item1->set( { homebranch => $library_A, holdingbranch => $library_A } )->store;
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?
235 set_holdallowed_rule($hold_allowed_from_home_library);
237 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
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" );
242 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
247 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
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" );
252 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
259 set_holdallowed_rule($hold_allowed_from_any_libraries);
261 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
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" );
266 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
271 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
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" );
276 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
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" );
285 my $itemtype2 = $builder->build(
286 { source => 'Itemtype',
287 value => { notforloan => 0 }
290 my $item3 = $builder->build_sample_item( { itype => $itemtype2 } );
292 my $hold = $builder->build(
293 { source => 'Reserve',
295 itemnumber => $item3->itemnumber,
301 Koha::CirculationRules->set_rules(
302 { categorycode => undef,
303 itemtype => $itemtype2,
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" );
315 $is = IsAvailableForItemLevelRequest( $item3, $patron1 );
316 is( $is, 1, "Item can be held, items in transit are not available" );
318 subtest 'Check holds availability with different item types' => sub {
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
328 my $biblio2 = $builder->build_sample_biblio( { itemtype => $itemtype } );
329 my $item4 = $builder->build_sample_item(
330 { biblionumber => $biblio2->biblionumber,
332 homebranch => $library_A,
333 holdingbranch => $library_A
336 my $item5 = $builder->build_sample_item(
337 { biblionumber => $biblio2->biblionumber,
339 homebranch => $library_A,
340 holdingbranch => $library_A
344 # Test hold_fulfillment_policy
345 $dbh->do("DELETE FROM circulation_rules");
346 Koha::CirculationRules->set_rules(
347 { categorycode => undef,
348 itemtype => $itemtype,
353 reservesallowed => 99,
354 holds_per_record => 99,
359 Koha::CirculationRules->set_rules(
360 { categorycode => undef,
361 itemtype => $itemtype2,
366 reservesallowed => 0,
367 holds_per_record => 0,
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" );
376 $is = IsAvailableForItemLevelRequest( $item4, $patron1 );
377 is( $is, 0, "Item4 cannot be requested to hold: 2 items, Item4 available, Item5 restricted" );
379 $is = IsAvailableForItemLevelRequest( $item5, $patron1 );
380 is( $is, 0, "Item5 cannot be requested to hold: 2 items, Item4 available, Item5 restricted" );
382 AddIssue( $patron2->unblessed, $item4->barcode );
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" );
387 $is = IsAvailableForItemLevelRequest( $item4, $patron1 );
388 is( $is, 1, "Item4 can be requested to hold, 2 items, Item4 checked out, Item5 restricted" );
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" );
395 subtest 'Check item checkout availability with ordered item' => sub {
398 my $biblio2 = $builder->build_sample_biblio( { itemtype => $itemtype } );
399 my $item1 = $builder->build_sample_item(
400 { biblionumber => $biblio2->biblionumber,
402 homebranch => $library_A,
403 holdingbranch => $library_A,
408 $dbh->do("DELETE FROM circulation_rules");
409 Koha::CirculationRules->set_rules(
410 { categorycode => undef,
411 itemtype => $itemtype2,
416 reservesallowed => 99,
417 holds_per_record => 99,
423 $is = ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblio2->biblionumber, patron => $patron1 } );
424 is( $is, 0, "Ordered item cannot be checked out" );
427 subtest 'Check item availability for hold with ordered item' => sub {
430 my $biblio2 = $builder->build_sample_biblio( { itemtype => $itemtype } );
431 my $item1 = $builder->build_sample_item(
432 { biblionumber => $biblio2->biblionumber,
434 homebranch => $library_A,
435 holdingbranch => $library_A,
440 $dbh->do("DELETE FROM circulation_rules");
441 Koha::CirculationRules->set_rules(
442 { categorycode => undef,
443 itemtype => $itemtype2,
448 reservesallowed => 99,
449 holds_per_record => 99,
455 $is = IsAvailableForItemLevelRequest( $item1, $patron1 );
456 is( $is, 1, "Ordered items are available for hold" );
461 $schema->storage->txn_rollback;
463 sub set_holdallowed_rule {
464 my ( $holdallowed, $branchcode ) = @_;
465 Koha::CirculationRules->set_rules(
466 { branchcode => $branchcode || undef,
469 holdallowed => $holdallowed,
470 hold_fulfillment_policy => 'any',
471 returnbranch => 'homebranch',