6 use t::lib::TestBuilder;
10 use Test::More tests => 72;
20 use Koha::CirculationRules;
22 use Koha::DateUtils qw( dt_from_string output_pref );
25 use Koha::Item::Transfer::Limits;
28 use Koha::Library::Groups;
33 use lib $FindBin::Bin;
36 my $schema = Koha::Database->new->schema;
37 $schema->storage->txn_begin;
39 my $builder = t::lib::TestBuilder->new();
40 my $dbh = C4::Context->dbh;
42 # Create two random branches
43 my $branch_1 = $builder->build({ source => 'Branch' })->{ branchcode };
44 my $branch_2 = $builder->build({ source => 'Branch' })->{ branchcode };
46 my $category = $builder->build({ source => 'Category' });
48 my $borrowers_count = 5;
50 $dbh->do('DELETE FROM itemtypes');
51 $dbh->do('DELETE FROM reserves');
52 $dbh->do('DELETE FROM circulation_rules');
53 my $insert_sth = $dbh->prepare('INSERT INTO itemtypes (itemtype) VALUES (?)');
54 $insert_sth->execute('CAN');
55 $insert_sth->execute('CANNOT');
56 $insert_sth->execute('DUMMY');
57 $insert_sth->execute('ONLY1');
59 # Setup Test------------------------
60 my $biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
62 # Create item instance for testing.
63 my $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
65 # Create some borrowers
67 foreach (1..$borrowers_count) {
68 my $borrowernumber = Koha::Patron->new({
69 firstname => 'my firstname',
70 surname => 'my surname ' . $_,
71 categorycode => $category->{categorycode},
72 branchcode => $branch_1,
73 })->store->borrowernumber;
74 push @borrowernumbers, $borrowernumber;
77 # Create five item level holds
78 foreach my $borrowernumber ( @borrowernumbers ) {
81 branchcode => $branch_1,
82 borrowernumber => $borrowernumber,
83 biblionumber => $biblio->biblionumber,
84 priority => C4::Reserves::CalculatePriority( $biblio->biblionumber ),
85 itemnumber => $itemnumber,
90 my $holds = $biblio->holds;
91 is( $holds->count, $borrowers_count, 'Test GetReserves()' );
92 is( $holds->next->priority, 1, "Reserve 1 has a priority of 1" );
93 is( $holds->next->priority, 2, "Reserve 2 has a priority of 2" );
94 is( $holds->next->priority, 3, "Reserve 3 has a priority of 3" );
95 is( $holds->next->priority, 4, "Reserve 4 has a priority of 4" );
96 is( $holds->next->priority, 5, "Reserve 5 has a priority of 5" );
98 my $item = Koha::Items->find( $itemnumber );
99 $holds = $item->current_holds;
100 my $first_hold = $holds->next;
101 my $reservedate = $first_hold->reservedate;
102 my $borrowernumber = $first_hold->borrowernumber;
103 my $branch_1code = $first_hold->branchcode;
104 my $reserve_id = $first_hold->reserve_id;
105 is( $reservedate, output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }), "holds_placed_today should return a valid reserve date");
106 is( $borrowernumber, $borrowernumbers[0], "holds_placed_today should return a valid borrowernumber");
107 is( $branch_1code, $branch_1, "holds_placed_today should return a valid branchcode");
108 ok($reserve_id, "Test holds_placed_today()");
110 my $hold = Koha::Holds->find( $reserve_id );
111 ok( $hold, "Koha::Holds found the hold" );
112 my $hold_biblio = $hold->biblio();
113 ok( $hold_biblio, "Got biblio using biblio() method" );
114 ok( $hold_biblio == $hold->biblio(), "biblio method returns stashed biblio" );
115 my $hold_item = $hold->item();
116 ok( $hold_item, "Got item using item() method" );
117 ok( $hold_item == $hold->item(), "item method returns stashed item" );
118 my $hold_branch = $hold->branch();
119 ok( $hold_branch, "Got branch using branch() method" );
120 ok( $hold_branch == $hold->branch(), "branch method returns stashed branch" );
121 my $hold_found = $hold->found();
122 $hold->set({ found => 'W'})->store();
123 is( Koha::Holds->waiting()->count(), 1, "Koha::Holds->waiting returns waiting holds" );
124 is( Koha::Holds->unfilled()->count(), 4, "Koha::Holds->unfilled returns unfilled holds" );
126 my $patron = Koha::Patrons->find( $borrowernumbers[0] );
127 $holds = $patron->holds;
128 is( $holds->next->borrowernumber, $borrowernumbers[0], "Test Koha::Patron->holds");
131 $holds = $item->current_holds;
132 $first_hold = $holds->next;
133 $borrowernumber = $first_hold->borrowernumber;
134 $branch_1code = $first_hold->branchcode;
135 $reserve_id = $first_hold->reserve_id;
138 reserve_id => $reserve_id,
140 branchcode => $branch_1,
141 itemnumber => $itemnumber,
142 suspend_until => output_pref( { dt => dt_from_string( "2013-01-01", "iso" ), dateonly => 1 } ),
145 $hold = Koha::Holds->find( $reserve_id );
146 ok( $hold->priority eq '4', "Test ModReserve, priority changed correctly" );
147 ok( $hold->suspend, "Test ModReserve, suspend hold" );
148 is( $hold->suspend_until, '2013-01-01 00:00:00', "Test ModReserve, suspend until date" );
150 ModReserve({ # call without reserve_id
152 biblionumber => $biblio->biblionumber,
153 itemnumber => $itemnumber,
154 borrowernumber => $borrowernumber,
156 $hold = Koha::Holds->find( $reserve_id );
157 ok( $hold->priority eq '3', "Test ModReserve, priority changed correctly" );
159 ToggleSuspend( $reserve_id );
160 $hold = Koha::Holds->find( $reserve_id );
161 ok( ! $hold->suspend, "Test ToggleSuspend(), no date" );
163 ToggleSuspend( $reserve_id, '2012-01-01' );
164 $hold = Koha::Holds->find( $reserve_id );
165 is( $hold->suspend_until, '2012-01-01 00:00:00', "Test ToggleSuspend(), with date" );
167 AutoUnsuspendReserves();
168 $hold = Koha::Holds->find( $reserve_id );
169 ok( ! $hold->suspend, "Test AutoUnsuspendReserves()" );
172 borrowernumber => $borrowernumber,
173 biblionumber => $biblio->biblionumber,
175 suspend_until => '2012-01-01',
177 $hold = Koha::Holds->find( $reserve_id );
178 is( $hold->suspend, 1, "Test SuspendAll()" );
179 is( $hold->suspend_until, '2012-01-01 00:00:00', "Test SuspendAll(), with date" );
182 borrowernumber => $borrowernumber,
183 biblionumber => $biblio->biblionumber,
186 $hold = Koha::Holds->find( $reserve_id );
187 is( $hold->suspend, 0, "Test resuming with SuspendAll()" );
188 is( $hold->suspend_until, undef, "Test resuming with SuspendAll(), should have no suspend until date" );
190 # Add a new hold for the borrower whose hold we canceled earlier, this time at the bib level
193 branchcode => $branch_1,
194 borrowernumber => $borrowernumbers[0],
195 biblionumber => $biblio->biblionumber,
199 $patron = Koha::Patrons->find( $borrowernumber );
200 $holds = $patron->holds;
201 my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $borrowernumbers[0] })->next->reserve_id;
202 ModReserveMinusPriority( $itemnumber, $reserveid );
203 $holds = $patron->holds;
204 is( $holds->search({ itemnumber => $itemnumber })->count, 1, "Test ModReserveMinusPriority()" );
206 $holds = $biblio->holds;
207 $hold = $holds->next;
208 AlterPriority( 'top', $hold->reserve_id, undef, 2, 1, 6 );
209 $hold = Koha::Holds->find( $reserveid );
210 is( $hold->priority, '1', "Test AlterPriority(), move to top" );
212 AlterPriority( 'down', $hold->reserve_id, undef, 2, 1, 6 );
213 $hold = Koha::Holds->find( $reserveid );
214 is( $hold->priority, '2', "Test AlterPriority(), move down" );
216 AlterPriority( 'up', $hold->reserve_id, 1, 3, 1, 6 );
217 $hold = Koha::Holds->find( $reserveid );
218 is( $hold->priority, '1', "Test AlterPriority(), move up" );
220 AlterPriority( 'bottom', $hold->reserve_id, undef, 2, 1, 6 );
221 $hold = Koha::Holds->find( $reserveid );
222 is( $hold->priority, '6', "Test AlterPriority(), move to bottom" );
224 # Regression test for bug 2394
226 # If IndependentBranches is ON and canreservefromotherbranches is OFF,
227 # a patron is not permittedo to request an item whose homebranch (i.e.,
228 # owner of the item) is different from the patron's own library.
229 # However, if canreservefromotherbranches is turned ON, the patron can
230 # create such hold requests.
232 # Note that canreservefromotherbranches has no effect if
233 # IndependentBranches is OFF.
235 my $foreign_biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
236 my $foreign_itemnumber = $builder->build_sample_item({ library => $branch_2, biblionumber => $foreign_biblio->biblionumber })->itemnumber;
237 Koha::CirculationRules->set_rules(
239 categorycode => undef,
243 reservesallowed => 25,
244 holds_per_record => 99,
248 Koha::CirculationRules->set_rules(
250 categorycode => undef,
252 itemtype => 'CANNOT',
254 reservesallowed => 0,
255 holds_per_record => 99,
260 # make sure some basic sysprefs are set
261 t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
262 t::lib::Mocks::mock_preference('item-level_itypes', 1);
264 # if IndependentBranches is OFF, a $branch_1 patron can reserve an $branch_2 item
265 t::lib::Mocks::mock_preference('IndependentBranches', 0);
268 CanItemBeReserved($borrowernumbers[0], $foreign_itemnumber)->{status}, 'OK',
269 '$branch_1 patron allowed to reserve $branch_2 item with IndependentBranches OFF (bug 2394)'
272 # if IndependentBranches is OFF, a $branch_1 patron cannot reserve an $branch_2 item
273 t::lib::Mocks::mock_preference('IndependentBranches', 1);
274 t::lib::Mocks::mock_preference('canreservefromotherbranches', 0);
276 CanItemBeReserved($borrowernumbers[0], $foreign_itemnumber)->{status} eq 'cannotReserveFromOtherBranches',
277 '$branch_1 patron NOT allowed to reserve $branch_2 item with IndependentBranches ON ... (bug 2394)'
280 # ... unless canreservefromotherbranches is ON
281 t::lib::Mocks::mock_preference('canreservefromotherbranches', 1);
283 CanItemBeReserved($borrowernumbers[0], $foreign_itemnumber)->{status} eq 'OK',
284 '... unless canreservefromotherbranches is ON (bug 2394)'
288 # Regression test for bug 11336 # Test if ModReserve correctly recalculate the priorities
289 $biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' });
290 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
291 my $reserveid1 = AddReserve(
293 branchcode => $branch_1,
294 borrowernumber => $borrowernumbers[0],
295 biblionumber => $biblio->biblionumber,
300 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
301 my $reserveid2 = AddReserve(
303 branchcode => $branch_1,
304 borrowernumber => $borrowernumbers[1],
305 biblionumber => $biblio->biblionumber,
310 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber })->itemnumber;
311 my $reserveid3 = AddReserve(
313 branchcode => $branch_1,
314 borrowernumber => $borrowernumbers[2],
315 biblionumber => $biblio->biblionumber,
320 my $hhh = Koha::Holds->search({ biblionumber => $biblio->biblionumber });
321 my $hold3 = Koha::Holds->find( $reserveid3 );
322 is( $hold3->priority, 3, "The 3rd hold should have a priority set to 3" );
323 ModReserve({ reserve_id => $reserveid1, rank => 'del' });
324 ModReserve({ reserve_id => $reserveid2, rank => 'del' });
325 is( $hold3->discard_changes->priority, 1, "After ModReserve, the 3rd reserve becomes the first on the waiting list" );
328 Koha::Items->find($itemnumber)->damaged(1)->store; # FIXME The $itemnumber is a bit confusing here
329 t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 1 );
330 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber)->{status}, 'OK', "Patron can reserve damaged item with AllowHoldsOnDamagedItems enabled" );
331 ok( defined( ( CheckReserves($itemnumber) )[1] ), "Hold can be trapped for damaged item with AllowHoldsOnDamagedItems enabled" );
333 $hold = Koha::Hold->new(
335 borrowernumber => $borrowernumbers[0],
336 itemnumber => $itemnumber,
337 biblionumber => $biblio->biblionumber,
340 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
342 "Patron cannot place a second item level hold for a given item" );
345 t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 0 );
346 ok( CanItemBeReserved( $borrowernumbers[0], $itemnumber)->{status} eq 'damaged', "Patron cannot reserve damaged item with AllowHoldsOnDamagedItems disabled" );
347 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for damaged item with AllowHoldsOnDamagedItems disabled" );
349 # Items that are not for loan, but holdable should not be trapped until they are available for loan
350 t::lib::Mocks::mock_preference( 'TrapHoldsOnOrder', 0 );
351 Koha::Items->find($itemnumber)->damaged(0)->notforloan(-1)->store;
352 Koha::Holds->search({ biblionumber => $biblio->id })->delete();
353 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber)->{status}, 'OK', "Patron can place hold on item that is not for loan but holdable ( notforloan < 0 )" );
354 $hold = Koha::Hold->new(
356 borrowernumber => $borrowernumbers[0],
357 itemnumber => $itemnumber,
358 biblionumber => $biblio->biblionumber,
361 reservedate => dt_from_string,
362 branchcode => $branch_1,
365 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item that is not for loan but holdable ( notforloan < 0 )" );
366 t::lib::Mocks::mock_preference( 'TrapHoldsOnOrder', 1 );
367 ok( defined( ( CheckReserves($itemnumber) )[1] ), "Hold is trapped for item that is not for loan but holdable ( notforloan < 0 )" );
368 t::lib::Mocks::mock_preference( 'SkipHoldTrapOnNotForLoanValue', '-1' );
369 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item with notforloan value matching SkipHoldTrapOnNotForLoanValue" );
370 t::lib::Mocks::mock_preference( 'SkipHoldTrapOnNotForLoanValue', '-1|1' );
371 ok( !defined( ( CheckReserves($itemnumber) )[1] ), "Hold cannot be trapped for item with notforloan value matching SkipHoldTrapOnNotForLoanValue" );
373 CanItemBeReserved( $borrowernumbers[0], $itemnumber)->{status}, 'itemAlreadyOnHold',
374 "cannot request item that you have already reservedd"
377 CanItemBeReserved( $borrowernumbers[0], $item->itemnumber, undef, { ignore_hold_counts => 1 })->{status}, 'OK',
378 "can request item if we are not checking holds counts, but only if policy allows or forbids it"
382 # Regression test for bug 9532
383 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
384 $item = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber});
387 branchcode => $branch_1,
388 borrowernumber => $borrowernumbers[0],
389 biblionumber => $biblio->biblionumber,
394 CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status}, 'tooManyReserves',
395 "cannot request item if policy that matches on item-level item type forbids it"
398 CanItemBeReserved( $borrowernumbers[0], $item->itemnumber, undef, { ignore_hold_counts => 1 })->{status}, 'tooManyReserves',
399 "cannot request item if policy that matches on item-level item type forbids it even if ignoring counts"
402 $item->itype('CAN')->store;
404 CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status} eq 'OK',
405 "can request item if policy that matches on item type allows it"
408 t::lib::Mocks::mock_preference('item-level_itypes', 0);
409 $item->itype(undef)->store;
411 CanItemBeReserved( $borrowernumbers[0], $item->itemnumber)->{status} eq 'tooManyReserves',
412 "cannot request item if policy that matches on bib-level item type forbids it (bug 9532)"
416 # Test branch item rules
418 $dbh->do('DELETE FROM circulation_rules');
419 Koha::CirculationRules->set_rules(
421 categorycode => undef,
425 reservesallowed => 25,
426 holds_per_record => 99,
430 Koha::CirculationRules->set_rules(
432 branchcode => $branch_1,
433 itemtype => 'CANNOT',
436 returnbranch => 'homebranch',
440 Koha::CirculationRules->set_rules(
442 branchcode => $branch_1,
446 returnbranch => 'homebranch',
450 $biblio = $builder->build_sample_biblio({ itemtype => 'CANNOT' });
451 $itemnumber = $builder->build_sample_item({ library => $branch_1, itype => 'CANNOT', biblionumber => $biblio->biblionumber})->itemnumber;
452 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status}, 'notReservable',
453 "CanItemBeReserved should return 'notReservable'");
455 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
456 $itemnumber = $builder->build_sample_item({ library => $branch_2, itype => 'CAN', biblionumber => $biblio->biblionumber})->itemnumber;
457 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status},
458 'cannotReserveFromOtherBranches',
459 "CanItemBeReserved should use PatronLibrary rule when ReservesControlBranch set to 'PatronLibrary'");
460 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
461 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status},
463 "CanItemBeReserved should use item home library rule when ReservesControlBranch set to 'ItemsHomeLibrary'");
465 $itemnumber = $builder->build_sample_item({ library => $branch_1, itype => 'CAN', biblionumber => $biblio->biblionumber})->itemnumber;
466 is(CanItemBeReserved($borrowernumbers[0], $itemnumber)->{status}, 'OK',
467 "CanItemBeReserved should return 'OK'");
470 t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
471 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
473 $dbh->do('DELETE FROM reserves');
474 $dbh->do('DELETE FROM issues');
475 $dbh->do('DELETE FROM items');
476 $dbh->do('DELETE FROM biblio');
478 $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
479 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
481 Koha::CirculationRules->set_rules(
483 categorycode => undef,
487 reservesallowed => 1,
488 holds_per_record => 99,
492 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
493 'OK', 'Patron can reserve item with hold limit of 1, no holds placed' );
495 my $res_id = AddReserve(
497 branchcode => $branch_1,
498 borrowernumber => $borrowernumbers[0],
499 biblionumber => $biblio->biblionumber,
504 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
505 'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
506 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber, undef, { ignore_hold_counts => 1 } )->{status},
507 'OK', 'Patron can reserve item if checking policy but not counts' );
509 #results should be the same for both ReservesControlBranch settings
510 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'ItemHomeLibrary' );
511 is( CanItemBeReserved( $borrowernumbers[0], $itemnumber )->{status},
512 'tooManyReserves', 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed' );
513 #reset for further tests
514 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
516 subtest 'Test max_holds per library/patron category' => sub {
519 $dbh->do('DELETE FROM reserves');
521 $biblio = $builder->build_sample_biblio;
522 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
523 Koha::CirculationRules->set_rules(
525 categorycode => undef,
527 itemtype => $biblio->itemtype,
529 reservesallowed => 99,
530 holds_per_record => 99,
538 branchcode => $branch_1,
539 borrowernumber => $borrowernumbers[0],
540 biblionumber => $biblio->biblionumber,
547 Koha::Holds->search( { borrowernumber => $borrowernumbers[0] } )->count();
548 is( $count, 3, 'Patron now has 3 holds' );
550 my $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
551 is( $ret->{status}, 'OK', 'Patron can place hold with no borrower circ rules' );
553 my $rule_all = Koha::CirculationRules->set_rule(
555 categorycode => $category->{categorycode},
557 rule_name => 'max_holds',
562 my $rule_branch = Koha::CirculationRules->set_rule(
564 branchcode => $branch_1,
565 categorycode => $category->{categorycode},
566 rule_name => 'max_holds',
571 $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
572 is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 3' );
574 $rule_branch->delete();
576 $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
577 is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a category rule of 3' );
580 $rule_branch->rule_value(3);
581 $rule_branch->store();
583 $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
584 is( $ret->{status}, 'tooManyReserves', 'Patron cannot place hold with only a branch/category rule of 3' );
586 $rule_branch->rule_value(5);
587 $rule_branch->update();
588 $rule_branch->rule_value(5);
589 $rule_branch->store();
591 $ret = CanItemBeReserved( $borrowernumbers[0], $itemnumber );
592 is( $ret->{status}, 'OK', 'Patron can place hold with branch/category rule of 5, category rule of 5' );
595 subtest 'Pickup location availability tests' => sub {
598 $biblio = $builder->build_sample_biblio({ itemtype => 'ONLY1' });
599 $itemnumber = $builder->build_sample_item({ library => $branch_1, biblionumber => $biblio->biblionumber})->itemnumber;
600 #Add a default rule to allow some holds
602 Koha::CirculationRules->set_rules(
605 categorycode => undef,
608 reservesallowed => 25,
609 holds_per_record => 99,
613 my $item = Koha::Items->find($itemnumber);
614 my $branch_to = $builder->build({ source => 'Branch' })->{ branchcode };
615 my $library = Koha::Libraries->find($branch_to);
616 $library->pickup_location('1')->store;
617 my $patron = $builder->build({ source => 'Borrower' })->{ borrowernumber };
619 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
620 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
622 $library->pickup_location('1')->store;
623 is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
624 'OK', 'Library is a pickup location');
626 my $limit = Koha::Item::Transfer::Limit->new({
627 fromBranch => $item->holdingbranch,
628 toBranch => $branch_to,
629 itemtype => $item->effective_itemtype,
631 is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
632 'cannotBeTransferred', 'Item cannot be transferred');
635 $library->pickup_location('0')->store;
636 is(CanItemBeReserved($patron, $item->itemnumber, $branch_to)->{status},
637 'libraryNotPickupLocation', 'Library is not a pickup location');
638 is(CanItemBeReserved($patron, $item->itemnumber, 'nonexistent')->{status},
639 'libraryNotFound', 'Cannot set unknown library as pickup location');
642 $schema->storage->txn_rollback;
644 subtest 'CanItemBeReserved / holds_per_day tests' => sub {
648 $schema->storage->txn_begin;
650 my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } );
651 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
652 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
654 # Create 3 biblios with items
655 my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
656 my $itemnumber_1 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_1->biblionumber})->itemnumber;
657 my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
658 my $itemnumber_2 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_2->biblionumber})->itemnumber;
659 my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype->itemtype });
660 my $itemnumber_3 = $builder->build_sample_item({ library => $library->branchcode, biblionumber => $biblio_3->biblionumber})->itemnumber;
662 Koha::CirculationRules->set_rules(
666 itemtype => $itemtype->itemtype,
668 reservesallowed => 1,
669 holds_per_record => 99,
676 CanItemBeReserved( $patron->borrowernumber, $itemnumber_1 ),
678 'Patron can reserve item with hold limit of 1, no holds placed'
683 branchcode => $library->branchcode,
684 borrowernumber => $patron->borrowernumber,
685 biblionumber => $biblio_1->biblionumber,
691 CanItemBeReserved( $patron->borrowernumber, $itemnumber_1 ),
692 { status => 'tooManyReserves', limit => 1 },
693 'Patron cannot reserve item with hold limit of 1, 1 bib level hold placed'
696 # Raise reservesallowed to avoid tooManyReserves from it
697 Koha::CirculationRules->set_rule(
702 itemtype => $itemtype->itemtype,
703 rule_name => 'reservesallowed',
709 CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
711 'Patron can reserve item with 2 reserves daily cap'
714 # Add a second reserve
715 my $res_id = AddReserve(
717 branchcode => $library->branchcode,
718 borrowernumber => $patron->borrowernumber,
719 biblionumber => $biblio_2->biblionumber,
724 CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
725 { status => 'tooManyReservesToday', limit => 2 },
726 'Patron cannot a third item with 2 reserves daily cap'
729 # Update last hold so reservedate is in the past, so 2 holds, but different day
730 $hold = Koha::Holds->find($res_id);
731 my $yesterday = dt_from_string() - DateTime::Duration->new( days => 1 );
732 $hold->reservedate($yesterday)->store;
735 CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
737 'Patron can reserve item with 2 bib level hold placed on different days, 2 reserves daily cap'
740 # Set holds_per_day to 0
741 Koha::CirculationRules->set_rule(
746 itemtype => $itemtype->itemtype,
747 rule_name => 'holds_per_day',
753 # Delete existing holds
754 Koha::Holds->search->delete;
756 CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
757 { status => 'tooManyReservesToday', limit => 0 },
758 'Patron cannot reserve if holds_per_day is 0 (i.e. 0 is 0)'
761 Koha::CirculationRules->set_rule(
766 itemtype => $itemtype->itemtype,
767 rule_name => 'holds_per_day',
772 Koha::Holds->search->delete;
774 CanItemBeReserved( $patron->borrowernumber, $itemnumber_2 ),
776 'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
780 branchcode => $library->branchcode,
781 borrowernumber => $patron->borrowernumber,
782 biblionumber => $biblio_1->biblionumber,
788 branchcode => $library->branchcode,
789 borrowernumber => $patron->borrowernumber,
790 biblionumber => $biblio_2->biblionumber,
796 CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
798 'Patron can reserve if holds_per_day is undef (i.e. undef is unlimited daily cap)'
802 branchcode => $library->branchcode,
803 borrowernumber => $patron->borrowernumber,
804 biblionumber => $biblio_3->biblionumber,
809 CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
810 { status => 'tooManyReserves', limit => 3 },
811 'Unlimited daily holds, but reached reservesallowed'
813 #results should be the same for both ReservesControlBranch settings
814 t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
816 CanItemBeReserved( $patron->borrowernumber, $itemnumber_3 ),
817 { status => 'tooManyReserves', limit => 3 },
818 'Unlimited daily holds, but reached reservesallowed'
821 $schema->storage->txn_rollback;
824 subtest 'CanItemBeReserved / branch_not_in_hold_group' => sub {
827 $schema->storage->txn_begin;
829 Koha::CirculationRules->set_rule(
832 categorycode => undef,
834 rule_name => 'reservesallowed',
840 my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
841 my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
844 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
845 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
846 my $library3 = $builder->build_object( { class => 'Koha::Libraries' } );
848 # Create library groups hierarchy
849 my $rootgroup = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
850 my $group1 = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
851 my $group2 = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
854 my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
855 my $patron3 = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
857 # Create 3 biblios with items
858 my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
859 my $item_1 = $builder->build_sample_item(
861 biblionumber => $biblio_1->biblionumber,
862 library => $library1->branchcode
865 my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
866 my $item_2 = $builder->build_sample_item(
868 biblionumber => $biblio_2->biblionumber,
869 library => $library2->branchcode
872 my $itemnumber_2 = $item_2->itemnumber;
873 my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
874 my $item_3 = $builder->build_sample_item(
876 biblionumber => $biblio_3->biblionumber,
877 library => $library1->branchcode
881 # Test 1: Patron 3 can place hold
883 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
885 'Patron can place hold if no circ_rules where defined'
888 # Insert default circ rule of holds allowed only from local hold group for all libraries
889 Koha::CirculationRules->set_rules(
895 hold_fulfillment_policy => 'any',
896 returnbranch => 'any'
901 # Test 2: Patron 1 can place hold
903 CanItemBeReserved( $patron1->borrowernumber, $itemnumber_2 ),
905 'Patron can place hold because patron\'s home library is part of hold group'
908 # Test 3: Patron 3 cannot place hold
910 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
911 { status => 'branchNotInHoldGroup' },
912 'Patron cannot place hold because patron\'s home library is not part of hold group'
915 # Insert default circ rule to "any" for library 2
916 Koha::CirculationRules->set_rules(
918 branchcode => $library2->branchcode,
922 hold_fulfillment_policy => 'any',
923 returnbranch => 'any'
928 # Test 4: Patron 3 can place hold
930 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
932 'Patron can place hold if holdallowed is set to "any" for library 2'
935 # Update default circ rule to "hold group" for library 2
936 Koha::CirculationRules->set_rules(
938 branchcode => $library2->branchcode,
942 hold_fulfillment_policy => 'any',
943 returnbranch => 'any'
948 # Test 5: Patron 3 cannot place hold
950 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
951 { status => 'branchNotInHoldGroup' },
952 'Patron cannot place hold if holdallowed is set to "hold group" for library 2'
955 # Insert default item rule to "any" for itemtype 2
956 Koha::CirculationRules->set_rules(
958 branchcode => $library2->branchcode,
959 itemtype => $itemtype2->itemtype,
962 hold_fulfillment_policy => 'any',
963 returnbranch => 'any'
968 # Test 6: Patron 3 can place hold
970 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
972 'Patron can place hold if holdallowed is set to "any" for itemtype 2'
975 # Update default item rule to "hold group" for itemtype 2
976 Koha::CirculationRules->set_rules(
978 branchcode => $library2->branchcode,
979 itemtype => $itemtype2->itemtype,
982 hold_fulfillment_policy => 'any',
983 returnbranch => 'any'
988 # Test 7: Patron 3 cannot place hold
990 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
991 { status => 'branchNotInHoldGroup' },
992 'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2'
995 # Insert branch item rule to "any" for itemtype 2 and library 2
996 Koha::CirculationRules->set_rules(
998 branchcode => $library2->branchcode,
999 itemtype => $itemtype2->itemtype,
1002 hold_fulfillment_policy => 'any',
1003 returnbranch => 'any'
1008 # Test 8: Patron 3 can place hold
1010 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
1012 'Patron can place hold if holdallowed is set to "any" for itemtype 2 and library 2'
1015 # Update branch item rule to "hold group" for itemtype 2 and library 2
1016 Koha::CirculationRules->set_rules(
1018 branchcode => $library2->branchcode,
1019 itemtype => $itemtype2->itemtype,
1022 hold_fulfillment_policy => 'any',
1023 returnbranch => 'any'
1028 # Test 9: Patron 3 cannot place hold
1030 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2 ),
1031 { status => 'branchNotInHoldGroup' },
1032 'Patron cannot place hold if holdallowed is set to "hold group" for itemtype 2 and library 2'
1035 $schema->storage->txn_rollback;
1039 subtest 'CanItemBeReserved / pickup_not_in_hold_group' => sub {
1042 $schema->storage->txn_begin;
1043 Koha::CirculationRules->set_rule(
1045 branchcode => undef,
1046 categorycode => undef,
1048 rule_name => 'reservesallowed',
1054 my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1055 my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } );
1058 my $library1 = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1059 my $library2 = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1060 my $library3 = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } );
1062 # Create library groups hierarchy
1063 my $rootgroup = $builder->build_object( { class => 'Koha::Library::Groups', value => {ft_local_hold_group => 1} } );
1064 my $group1 = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library1->branchcode}} );
1065 my $group2 = $builder->build_object( { class => 'Koha::Library::Groups', value => {parent_id => $rootgroup->id, branchcode => $library2->branchcode} } );
1068 my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } );
1069 my $patron3 = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library3->branchcode} } );
1071 # Create 3 biblios with items
1072 my $biblio_1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1073 my $item_1 = $builder->build_sample_item(
1075 biblionumber => $biblio_1->biblionumber,
1076 library => $library1->branchcode
1079 my $biblio_2 = $builder->build_sample_biblio({ itemtype => $itemtype2->itemtype });
1080 my $item_2 = $builder->build_sample_item(
1082 biblionumber => $biblio_2->biblionumber,
1083 library => $library2->branchcode
1086 my $itemnumber_2 = $item_2->itemnumber;
1087 my $biblio_3 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype });
1088 my $item_3 = $builder->build_sample_item(
1090 biblionumber => $biblio_3->biblionumber,
1091 library => $library1->branchcode
1095 # Test 1: Patron 3 can place hold
1097 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1099 'Patron can place hold if no circ_rules where defined'
1102 # Insert default circ rule of holds allowed only from local hold group for all libraries
1103 Koha::CirculationRules->set_rules(
1105 branchcode => undef,
1109 hold_fulfillment_policy => 'holdgroup',
1110 returnbranch => 'any'
1115 # Test 2: Patron 1 can place hold
1117 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library1->branchcode ),
1119 'Patron can place hold because pickup location is part of hold group'
1122 # Test 3: Patron 3 cannot place hold
1124 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1125 { status => 'pickupNotInHoldGroup' },
1126 'Patron cannot place hold because pickup location is not part of hold group'
1129 # Insert default circ rule to "any" for library 2
1130 Koha::CirculationRules->set_rules(
1132 branchcode => $library2->branchcode,
1136 hold_fulfillment_policy => 'any',
1137 returnbranch => 'any'
1142 # Test 4: Patron 3 can place hold
1144 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1146 'Patron can place hold if default_branch_circ_rules is set to "any" for library 2'
1149 # Update default circ rule to "hold group" for library 2
1150 Koha::CirculationRules->set_rules(
1152 branchcode => $library2->branchcode,
1156 hold_fulfillment_policy => 'holdgroup',
1157 returnbranch => 'any'
1162 # Test 5: Patron 3 cannot place hold
1164 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1165 { status => 'pickupNotInHoldGroup' },
1166 'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for library 2'
1169 # Insert default item rule to "any" for itemtype 2
1170 Koha::CirculationRules->set_rules(
1172 branchcode => $library2->branchcode,
1173 itemtype => $itemtype2->itemtype,
1176 hold_fulfillment_policy => 'any',
1177 returnbranch => 'any'
1182 # Test 6: Patron 3 can place hold
1184 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1186 'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2'
1189 # Update default item rule to "hold group" for itemtype 2
1190 Koha::CirculationRules->set_rules(
1192 branchcode => $library2->branchcode,
1193 itemtype => $itemtype2->itemtype,
1196 hold_fulfillment_policy => 'holdgroup',
1197 returnbranch => 'any'
1202 # Test 7: Patron 3 cannot place hold
1204 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1205 { status => 'pickupNotInHoldGroup' },
1206 'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2'
1209 # Insert branch item rule to "any" for itemtype 2 and library 2
1210 Koha::CirculationRules->set_rules(
1212 branchcode => $library2->branchcode,
1213 itemtype => $itemtype2->itemtype,
1216 hold_fulfillment_policy => 'any',
1217 returnbranch => 'any'
1222 # Test 8: Patron 3 can place hold
1224 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1226 'Patron can place hold if hold_fulfillment_policy is set to "any" for itemtype 2 and library 2'
1229 # Update branch item rule to "hold group" for itemtype 2 and library 2
1230 Koha::CirculationRules->set_rules(
1232 branchcode => $library2->branchcode,
1233 itemtype => $itemtype2->itemtype,
1236 hold_fulfillment_policy => 'holdgroup',
1237 returnbranch => 'any'
1242 # Test 9: Patron 3 cannot place hold
1244 CanItemBeReserved( $patron3->borrowernumber, $itemnumber_2, $library3->branchcode ),
1245 { status => 'pickupNotInHoldGroup' },
1246 'Patron cannot place hold if hold_fulfillment_policy is set to "hold group" for itemtype 2 and library 2'
1249 $schema->storage->txn_rollback;
1252 subtest 'non priority holds' => sub {
1256 $schema->storage->txn_begin;
1258 Koha::CirculationRules->set_rules(
1260 branchcode => undef,
1261 categorycode => undef,
1264 renewalsallowed => 5,
1265 reservesallowed => 5,
1270 my $item = $builder->build_sample_item;
1272 my $patron1 = $builder->build_object(
1274 class => 'Koha::Patrons',
1275 value => { branchcode => $item->homebranch }
1278 my $patron2 = $builder->build_object(
1280 class => 'Koha::Patrons',
1281 value => { branchcode => $item->homebranch }
1285 Koha::Checkout->new(
1287 borrowernumber => $patron1->borrowernumber,
1288 itemnumber => $item->itemnumber,
1289 branchcode => $item->homebranch
1293 my $hid = AddReserve(
1295 branchcode => $item->homebranch,
1296 borrowernumber => $patron2->borrowernumber,
1297 biblionumber => $item->biblionumber,
1299 itemnumber => $item->itemnumber,
1304 CanBookBeRenewed( $patron1->borrowernumber, $item->itemnumber );
1306 ok( !$ok, 'Cannot renew' );
1307 is( $err, 'on_reserve', 'Item is on hold' );
1309 my $hold = Koha::Holds->find($hid);
1310 $hold->non_priority(1)->store;
1313 CanBookBeRenewed( $patron1->borrowernumber, $item->itemnumber );
1315 ok( $ok, 'Can renew' );
1316 is( $err, undef, 'Item is on non priority hold' );
1318 my $patron3 = $builder->build_object(
1320 class => 'Koha::Patrons',
1321 value => { branchcode => $item->homebranch }
1325 # Add second hold with non_priority = 0
1328 branchcode => $item->homebranch,
1329 borrowernumber => $patron3->borrowernumber,
1330 biblionumber => $item->biblionumber,
1332 itemnumber => $item->itemnumber,
1337 CanBookBeRenewed( $patron1->borrowernumber, $item->itemnumber );
1339 ok( !$ok, 'Cannot renew' );
1340 is( $err, 'on_reserve', 'Item is on hold' );
1342 $schema->storage->txn_rollback;
1346 subtest 'CanItemBeReserved rule precedence tests' => sub {
1350 t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary');
1351 $schema->storage->txn_begin;
1352 my $library = $builder->build_object( { class => 'Koha::Libraries', value => {
1353 pickup_location => 1,
1355 my $item = $builder->build_sample_item({
1356 homebranch => $library->branchcode,
1357 holdingbranch => $library->branchcode
1359 my $item2 = $builder->build_sample_item({
1360 homebranch => $library->branchcode,
1361 holdingbranch => $library->branchcode,
1362 itype => $item->itype
1364 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1365 branchcode => $library->branchcode
1367 Koha::CirculationRules->set_rules(
1369 branchcode => undef,
1370 categorycode => $patron->categorycode,
1371 itemtype => $item->itype,
1373 reservesallowed => 1,
1378 CanItemBeReserved( $patron->borrowernumber, $item->itemnumber, $library->branchcode ),
1380 'Patron of specified category can place 1 hold on specified itemtype'
1382 my $hold = $builder->build_object({ class => 'Koha::Holds', value => {
1383 biblionumber => $item2->biblionumber,
1384 itemnumber => $item2->itemnumber,
1387 branchcode => $library->branchcode,
1388 borrowernumber => $patron->borrowernumber,
1391 CanItemBeReserved( $patron->borrowernumber, $item->itemnumber, $library->branchcode ),
1392 { status => 'tooManyReserves', limit => 1 },
1393 'Patron of specified category can place 1 hold on specified itemtype, cannot place a second'
1395 Koha::CirculationRules->set_rules(
1397 branchcode => $library->branchcode,
1398 categorycode => undef,
1401 reservesallowed => 2,
1406 CanItemBeReserved( $patron->borrowernumber, $item->itemnumber, $library->branchcode ),
1408 'Patron of specified category can place 1 hold on specified itemtype if library rule for all types and categories set to 2'
1411 $schema->storage->txn_rollback;