3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
21 use Test::More tests => 52;
24 use Test::Deep qw( cmp_deeply );
30 use POSIX qw( floor );
32 use t::lib::TestBuilder;
41 use C4::Overdues qw(UpdateFine CalcFine);
45 use Koha::Item::Transfers;
49 use Koha::CirculationRules;
50 use Koha::Subscriptions;
51 use Koha::Account::Lines;
52 use Koha::Account::Offsets;
57 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
61 my ( $error, $question, $alert ) = @_;
63 $s = %$error ? ' (error: ' . join( ' ', keys %$error ) . ')' : '';
64 $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
65 $s .= %$alert ? ' (alert: ' . join( ' ', keys %$alert ) . ')' : '';
69 sub test_debarment_on_checkout {
71 my $item = $params->{item};
72 my $library = $params->{library};
73 my $patron = $params->{patron};
74 my $due_date = $params->{due_date} || dt_from_string;
75 my $return_date = $params->{return_date} || dt_from_string;
76 my $expected_expiration_date = $params->{expiration_date};
78 $expected_expiration_date = output_pref(
80 dt => $expected_expiration_date,
86 my $line_number = $caller[2];
87 AddIssue( $patron, $item->barcode, $due_date );
89 my ( undef, $message ) = AddReturn( $item->barcode, $library->{branchcode}, undef, $return_date );
90 is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
91 or diag('AddReturn returned message ' . Dumper $message );
92 my $debarments = Koha::Patron::Debarments::GetDebarments(
93 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
94 is( scalar(@$debarments), 1, 'Test at line ' . $line_number );
96 is( $debarments->[0]->{expiration},
97 $expected_expiration_date, 'Test at line ' . $line_number );
98 Koha::Patron::Debarments::DelUniqueDebarment(
99 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
102 my $schema = Koha::Database->schema;
103 $schema->storage->txn_begin;
104 my $builder = t::lib::TestBuilder->new;
105 my $dbh = C4::Context->dbh;
107 # Prevent random failures by mocking ->now
108 my $now_value = dt_from_string;
109 my $mocked_datetime = Test::MockModule->new('DateTime');
110 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
112 my $cache = Koha::Caches->get_instance();
113 $dbh->do(q|DELETE FROM special_holidays|);
114 $dbh->do(q|DELETE FROM repeatable_holidays|);
115 my $branches = Koha::Libraries->search();
116 for my $branch ( $branches->next ) {
117 my $key = $branch->branchcode . "_holidays";
118 $cache->clear_from_cache($key);
121 # Start with a clean slate
122 $dbh->do('DELETE FROM issues');
123 $dbh->do('DELETE FROM borrowers');
125 # Disable recording of the staff who checked out an item until we're ready for it
126 t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 0);
128 my $module = Test::MockModule->new('C4::Context');
130 my $library = $builder->build({
133 my $library2 = $builder->build({
136 my $itemtype = $builder->build(
138 source => 'Itemtype',
142 rentalcharge_daily => 0,
143 defaultreplacecost => undef,
148 my $patron_category = $builder->build(
150 source => 'Category',
152 category_type => 'P',
154 BlockExpiredPatronOpacActions => -1, # Pick the pref value
159 my $CircControl = C4::Context->preference('CircControl');
160 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
163 homebranch => $library2->{branchcode},
164 holdingbranch => $library2->{branchcode}
168 branchcode => $library2->{branchcode}
171 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
173 # No userenv, PickupLibrary
174 t::lib::Mocks::mock_preference('IndependentBranches', '0');
175 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
177 C4::Context->preference('CircControl'),
179 'CircControl changed to PickupLibrary'
182 C4::Circulation::_GetCircControlBranch($item, $borrower),
183 $item->{$HomeOrHoldingBranch},
184 '_GetCircControlBranch returned item branch (no userenv defined)'
187 # No userenv, PatronLibrary
188 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
190 C4::Context->preference('CircControl'),
192 'CircControl changed to PatronLibrary'
195 C4::Circulation::_GetCircControlBranch($item, $borrower),
196 $borrower->{branchcode},
197 '_GetCircControlBranch returned borrower branch'
200 # No userenv, ItemHomeLibrary
201 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
203 C4::Context->preference('CircControl'),
205 'CircControl changed to ItemHomeLibrary'
208 $item->{$HomeOrHoldingBranch},
209 C4::Circulation::_GetCircControlBranch($item, $borrower),
210 '_GetCircControlBranch returned item branch'
214 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
215 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
217 # Userenv set, PickupLibrary
218 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
220 C4::Context->preference('CircControl'),
222 'CircControl changed to PickupLibrary'
225 C4::Circulation::_GetCircControlBranch($item, $borrower),
226 $library2->{branchcode},
227 '_GetCircControlBranch returned current branch'
230 # Userenv set, PatronLibrary
231 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
233 C4::Context->preference('CircControl'),
235 'CircControl changed to PatronLibrary'
238 C4::Circulation::_GetCircControlBranch($item, $borrower),
239 $borrower->{branchcode},
240 '_GetCircControlBranch returned borrower branch'
243 # Userenv set, ItemHomeLibrary
244 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
246 C4::Context->preference('CircControl'),
248 'CircControl changed to ItemHomeLibrary'
251 C4::Circulation::_GetCircControlBranch($item, $borrower),
252 $item->{$HomeOrHoldingBranch},
253 '_GetCircControlBranch returned item branch'
256 # Reset initial configuration
257 t::lib::Mocks::mock_preference('CircControl', $CircControl);
259 C4::Context->preference('CircControl'),
261 'CircControl reset to its initial value'
264 # Set a simple circ policy
265 $dbh->do('DELETE FROM circulation_rules');
266 Koha::CirculationRules->set_rules(
268 categorycode => undef,
272 reservesallowed => 25,
274 lengthunit => 'days',
275 renewalsallowed => 1,
277 norenewalbefore => undef,
285 subtest "GetIssuingCharges tests" => sub {
287 my $branch_discount = $builder->build_object({ class => 'Koha::Libraries' });
288 my $branch_no_discount = $builder->build_object({ class => 'Koha::Libraries' });
289 Koha::CirculationRules->set_rule(
291 categorycode => undef,
292 branchcode => $branch_discount->branchcode,
294 rule_name => 'rentaldiscount',
298 my $itype_charge = $builder->build_object({
299 class => 'Koha::ItemTypes',
304 my $itype_no_charge = $builder->build_object({
305 class => 'Koha::ItemTypes',
310 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
311 my $item_1 = $builder->build_sample_item({ itype => $itype_charge->itemtype });
312 my $item_2 = $builder->build_sample_item({ itype => $itype_no_charge->itemtype });
314 t::lib::Mocks::mock_userenv({ branchcode => $branch_no_discount->branchcode });
315 # For now the sub always uses the env branch, this should follow CircControl instead
316 my ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
317 is( $charge + 0, 10.00, "Charge fetched correctly when no discount exists");
318 ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
319 is( $charge + 0, 0.00, "Charge fetched correctly when no discount exists and no charge");
321 t::lib::Mocks::mock_userenv({ branchcode => $branch_discount->branchcode });
322 # For now the sub always uses the env branch, this should follow CircControl instead
323 ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
324 is( $charge + 0, 8.50, "Charge fetched correctly when discount exists");
325 ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
326 is( $charge + 0, 0.00, "Charge fetched correctly when discount exists and no charge");
330 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
331 subtest "CanBookBeRenewed tests" => sub {
334 C4::Context->set_preference('ItemsDeniedRenewal','');
335 # Generate test biblio
336 my $biblio = $builder->build_sample_biblio();
338 my $branch = $library2->{branchcode};
340 my $item_1 = $builder->build_sample_item(
342 biblionumber => $biblio->biblionumber,
344 replacementprice => 12.00,
348 $reused_itemnumber_1 = $item_1->itemnumber;
350 my $item_2 = $builder->build_sample_item(
352 biblionumber => $biblio->biblionumber,
354 replacementprice => 23.00,
358 $reused_itemnumber_2 = $item_2->itemnumber;
360 my $item_3 = $builder->build_sample_item(
362 biblionumber => $biblio->biblionumber,
364 replacementprice => 23.00,
370 my %renewing_borrower_data = (
372 surname => 'Renewal',
373 categorycode => $patron_category->{categorycode},
374 branchcode => $branch,
377 my %reserving_borrower_data = (
378 firstname => 'Katrin',
379 surname => 'Reservation',
380 categorycode => $patron_category->{categorycode},
381 branchcode => $branch,
384 my %hold_waiting_borrower_data = (
386 surname => 'Reservation',
387 categorycode => $patron_category->{categorycode},
388 branchcode => $branch,
391 my %restricted_borrower_data = (
392 firstname => 'Alice',
393 surname => 'Reservation',
394 categorycode => $patron_category->{categorycode},
395 debarred => '3228-01-01',
396 branchcode => $branch,
399 my %expired_borrower_data = (
402 categorycode => $patron_category->{categorycode},
403 branchcode => $branch,
404 dateexpiry => dt_from_string->subtract( months => 1 ),
407 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
408 my $reserving_borrowernumber = Koha::Patron->new(\%reserving_borrower_data)->store->borrowernumber;
409 my $hold_waiting_borrowernumber = Koha::Patron->new(\%hold_waiting_borrower_data)->store->borrowernumber;
410 my $restricted_borrowernumber = Koha::Patron->new(\%restricted_borrower_data)->store->borrowernumber;
411 my $expired_borrowernumber = Koha::Patron->new(\%expired_borrower_data)->store->borrowernumber;
413 my $renewing_borrower_obj = Koha::Patrons->find( $renewing_borrowernumber );
414 my $renewing_borrower = $renewing_borrower_obj->unblessed;
415 my $restricted_borrower = Koha::Patrons->find( $restricted_borrowernumber )->unblessed;
416 my $expired_borrower = Koha::Patrons->find( $expired_borrowernumber )->unblessed;
423 my $checkitem = undef;
426 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
427 my $datedue = dt_from_string( $issue->date_due() );
428 is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
430 my $issue2 = AddIssue( $renewing_borrower, $item_2->barcode);
431 $datedue = dt_from_string( $issue->date_due() );
432 is (defined $issue2, 1, "Item 2 checked out, due date: " . $issue2->date_due());
435 my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
436 is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}");
438 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
439 is( $renewokay, 1, 'Can renew, no holds for this title or item');
442 # Biblio-level hold, renewal test
445 branchcode => $branch,
446 borrowernumber => $reserving_borrowernumber,
447 biblionumber => $biblio->biblionumber,
448 priority => $priority,
449 reservation_date => $resdate,
450 expiration_date => $expdate,
452 itemnumber => $checkitem,
457 # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
458 Koha::CirculationRules->set_rule(
460 categorycode => undef,
463 rule_name => 'onshelfholds',
467 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
468 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
469 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
470 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
471 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
473 # Now let's add an item level hold, we should no longer be able to renew the item
474 my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
476 borrowernumber => $hold_waiting_borrowernumber,
477 biblionumber => $biblio->biblionumber,
478 itemnumber => $item_1->itemnumber,
479 branchcode => $branch,
483 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
484 is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
487 # Now let's add a waiting hold on the 3rd item, it's no longer available tp check out by just anyone, so we should no longer
488 # be able to renew these items
489 $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
491 borrowernumber => $hold_waiting_borrowernumber,
492 biblionumber => $biblio->biblionumber,
493 itemnumber => $item_3->itemnumber,
494 branchcode => $branch,
499 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
500 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
501 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
502 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
503 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
505 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
506 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
507 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
509 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
510 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
511 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
513 my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
514 my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber )->unblessed;
515 AddIssue($reserving_borrower, $item_3->barcode);
516 my $reserve = $dbh->selectrow_hashref(
517 'SELECT * FROM old_reserves WHERE reserve_id = ?',
521 is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
523 # Item-level hold, renewal test
526 branchcode => $branch,
527 borrowernumber => $reserving_borrowernumber,
528 biblionumber => $biblio->biblionumber,
529 priority => $priority,
530 reservation_date => $resdate,
531 expiration_date => $expdate,
533 itemnumber => $item_1->itemnumber,
538 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
539 is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
540 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
542 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber, 1);
543 is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
545 # Items can't fill hold for reasons
546 $item_1->notforloan(1)->store;
547 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
548 is( $renewokay, 1, 'Can renew, item is marked not for loan, hold does not block');
549 $item_1->set({notforloan => 0, itype => $itemtype })->store;
551 # FIXME: Add more for itemtype not for loan etc.
553 # Restricted users cannot renew when RestrictionBlockRenewing is enabled
554 my $item_5 = $builder->build_sample_item(
556 biblionumber => $biblio->biblionumber,
558 replacementprice => 23.00,
562 my $datedue5 = AddIssue($restricted_borrower, $item_5->barcode);
563 is (defined $datedue5, 1, "Item with date due checked out, due date: $datedue5");
565 t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
566 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
567 is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
568 ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $item_5->itemnumber);
569 is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
571 # Users cannot renew an overdue item
572 my $item_6 = $builder->build_sample_item(
574 biblionumber => $biblio->biblionumber,
576 replacementprice => 23.00,
581 my $item_7 = $builder->build_sample_item(
583 biblionumber => $biblio->biblionumber,
585 replacementprice => 23.00,
590 my $datedue6 = AddIssue( $renewing_borrower, $item_6->barcode);
591 is (defined $datedue6, 1, "Item 2 checked out, due date: ".$datedue6->date_due);
593 my $now = dt_from_string();
594 my $five_weeks = DateTime::Duration->new(weeks => 5);
595 my $five_weeks_ago = $now - $five_weeks;
596 t::lib::Mocks::mock_preference('finesMode', 'production');
598 my $passeddatedue1 = AddIssue($renewing_borrower, $item_7->barcode, $five_weeks_ago);
599 is (defined $passeddatedue1, 1, "Item with passed date due checked out, due date: " . $passeddatedue1->date_due);
601 my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower->{categorycode}, $branch, $five_weeks_ago, $now );
602 C4::Overdues::UpdateFine(
604 issue_id => $passeddatedue1->id(),
605 itemnumber => $item_7->itemnumber,
606 borrowernumber => $renewing_borrower->{borrowernumber},
608 due => Koha::DateUtils::output_pref($five_weeks_ago)
612 t::lib::Mocks::mock_preference('RenewalLog', 0);
613 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
614 my %params_renewal = (
615 timestamp => { -like => $date . "%" },
616 module => "CIRCULATION",
620 timestamp => { -like => $date . "%" },
621 module => "CIRCULATION",
624 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );
625 my $dt = dt_from_string();
626 Time::Fake->offset( $dt->epoch );
627 my $datedue1 = AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
628 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
629 is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
630 isnt (DateTime->compare($datedue1, $dt), 0, "AddRenewal returned a good duedate");
633 t::lib::Mocks::mock_preference('RenewalLog', 1);
634 $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
635 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
636 AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
637 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
638 is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
640 my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
641 is( $fines->count, 2, 'AddRenewal left both fines' );
642 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
643 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
647 my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
648 my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
649 AddIssue( $renewing_borrower,$item_7->barcode,Koha::DateUtils::output_pref({str=>$datedue6->date_due, dateformat =>'iso'}),0,$date, 0, undef );
650 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
651 is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
652 $new_log_size = Koha::ActionLogs->count( \%params_issue );
653 is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
655 $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
658 t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
659 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
660 is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
661 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
662 is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
665 $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
669 # Test automatic renewal before value for "norenewalbefore" in policy is set
670 # In this case automatic renewal is not permitted prior to due date
671 my $item_4 = $builder->build_sample_item(
673 biblionumber => $biblio->biblionumber,
675 replacementprice => 16.00,
680 $issue = AddIssue( $renewing_borrower, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
681 ( $renewokay, $error ) =
682 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
683 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
684 is( $error, 'auto_too_soon',
685 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
688 branchcode => $branch,
689 borrowernumber => $reserving_borrowernumber,
690 biblionumber => $biblio->biblionumber,
691 itemnumber => $bibitems,
692 priority => $priority,
693 reservation_date => $resdate,
694 expiration_date => $expdate,
697 itemnumber => $item_4->itemnumber,
701 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
702 is( $renewokay, 0, 'Still should not be able to renew' );
703 is( $error, 'on_reserve', 'returned code is on_reserve, reserve checked when not checking for cron' );
704 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, undef, 1 );
705 is( $renewokay, 0, 'Still should not be able to renew' );
706 is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked when checking for cron' );
707 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
708 is( $renewokay, 0, 'Still should not be able to renew' );
709 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
710 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1, 1 );
711 is( $renewokay, 0, 'Still should not be able to renew' );
712 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
713 $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
714 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
715 is( $renewokay, 0, 'Still should not be able to renew' );
716 is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
717 ModReserveCancelAll($item_4->itemnumber, $reserving_borrowernumber);
721 $renewing_borrower_obj->autorenew_checkouts(0)->store;
722 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
723 is( $renewokay, 1, 'No renewal before is undef, but patron opted out of auto_renewal' );
724 $renewing_borrower_obj->autorenew_checkouts(1)->store;
728 # Test premature manual renewal
729 Koha::CirculationRules->set_rule(
731 categorycode => undef,
734 rule_name => 'norenewalbefore',
739 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
740 is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
741 is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
744 # Test 'exact time' setting for syspref NoRenewalBeforePrecision
745 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
747 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
748 $datedue->clone->add( days => -7 ),
749 'Bug 14395: Renewals permitted 7 days before due date, as expected'
753 # Test 'date' setting for syspref NoRenewalBeforePrecision
754 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
756 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
757 $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
758 'Bug 14395: Renewals permitted 7 days before due date, as expected'
762 # Test premature automatic renewal
763 ( $renewokay, $error ) =
764 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
765 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
766 is( $error, 'auto_too_soon',
767 'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
770 $renewing_borrower_obj->autorenew_checkouts(0)->store;
771 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
772 is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
773 is( $error, 'too_soon', 'Error is too_soon, no auto' );
774 $renewing_borrower_obj->autorenew_checkouts(1)->store;
776 # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
777 # and test automatic renewal again
778 $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
779 ( $renewokay, $error ) =
780 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
781 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
782 is( $error, 'auto_too_soon',
783 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
786 $renewing_borrower_obj->autorenew_checkouts(0)->store;
787 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
788 is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
789 is( $error, 'too_soon', 'Error is too_soon, no auto' );
790 $renewing_borrower_obj->autorenew_checkouts(1)->store;
792 # Change policy so that loans can be renewed 99 days prior to the due date
793 # and test automatic renewal again
794 $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
795 ( $renewokay, $error ) =
796 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
797 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
798 is( $error, 'auto_renew',
799 'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
802 $renewing_borrower_obj->autorenew_checkouts(0)->store;
803 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
804 is( $renewokay, 1, 'No renewal before is 99, patron opted out of auto_renewal so can renew' );
805 $renewing_borrower_obj->autorenew_checkouts(1)->store;
807 subtest "too_late_renewal / no_auto_renewal_after" => sub {
809 my $item_to_auto_renew = $builder->build_sample_item(
811 biblionumber => $biblio->biblionumber,
816 my $ten_days_before = dt_from_string->add( days => -10 );
817 my $ten_days_ahead = dt_from_string->add( days => 10 );
818 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
820 Koha::CirculationRules->set_rules(
822 categorycode => undef,
826 norenewalbefore => '7',
827 no_auto_renewal_after => '9',
831 ( $renewokay, $error ) =
832 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
833 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
834 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
836 Koha::CirculationRules->set_rules(
838 categorycode => undef,
842 norenewalbefore => '7',
843 no_auto_renewal_after => '10',
847 ( $renewokay, $error ) =
848 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
849 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
850 is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
852 Koha::CirculationRules->set_rules(
854 categorycode => undef,
858 norenewalbefore => '7',
859 no_auto_renewal_after => '11',
863 ( $renewokay, $error ) =
864 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
865 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
866 is( $error, 'auto_too_soon', 'Cannot auto renew, too soon - no_auto_renewal_after is defined(returned code is auto_too_soon)' );
868 Koha::CirculationRules->set_rules(
870 categorycode => undef,
874 norenewalbefore => '10',
875 no_auto_renewal_after => '11',
879 ( $renewokay, $error ) =
880 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
881 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
882 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
884 Koha::CirculationRules->set_rules(
886 categorycode => undef,
890 norenewalbefore => '10',
891 no_auto_renewal_after => undef,
892 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
896 ( $renewokay, $error ) =
897 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
898 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
899 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
901 Koha::CirculationRules->set_rules(
903 categorycode => undef,
907 norenewalbefore => '7',
908 no_auto_renewal_after => '15',
909 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
913 ( $renewokay, $error ) =
914 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
915 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
916 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
918 Koha::CirculationRules->set_rules(
920 categorycode => undef,
924 norenewalbefore => '10',
925 no_auto_renewal_after => undef,
926 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 1 ),
930 ( $renewokay, $error ) =
931 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
932 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
933 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
936 subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew & OPACFineNoRenewalsIncludeCredit" => sub {
938 my $item_to_auto_renew = $builder->build_sample_item(
940 biblionumber => $biblio->biblionumber,
945 my $ten_days_before = dt_from_string->add( days => -10 );
946 my $ten_days_ahead = dt_from_string->add( days => 10 );
947 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
949 Koha::CirculationRules->set_rules(
951 categorycode => undef,
955 norenewalbefore => '10',
956 no_auto_renewal_after => '11',
960 C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
961 C4::Context->set_preference('OPACFineNoRenewals','10');
962 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
963 my $fines_amount = 5;
964 my $account = Koha::Account->new({patron_id => $renewing_borrowernumber});
967 amount => $fines_amount,
970 item_id => $item_to_auto_renew->itemnumber,
971 description => "Some fines"
973 )->status('RETURNED')->store;
974 ( $renewokay, $error ) =
975 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
976 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
977 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
981 amount => $fines_amount,
984 item_id => $item_to_auto_renew->itemnumber,
985 description => "Some fines"
987 )->status('RETURNED')->store;
988 ( $renewokay, $error ) =
989 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
990 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
991 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
995 amount => $fines_amount,
998 item_id => $item_to_auto_renew->itemnumber,
999 description => "Some fines"
1001 )->status('RETURNED')->store;
1002 ( $renewokay, $error ) =
1003 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1004 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1005 is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
1007 $account->add_credit(
1009 amount => $fines_amount,
1010 interface => 'test',
1012 description => "Some payment"
1015 ( $renewokay, $error ) =
1016 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1017 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1018 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
1020 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','0');
1021 ( $renewokay, $error ) =
1022 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1023 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1024 is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
1026 $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
1027 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
1030 subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
1032 my $item_to_auto_renew = $builder->build_sample_item(
1034 biblionumber => $biblio->biblionumber,
1039 Koha::CirculationRules->set_rules(
1041 categorycode => undef,
1042 branchcode => undef,
1045 norenewalbefore => 10,
1046 no_auto_renewal_after => 11,
1051 my $ten_days_before = dt_from_string->add( days => -10 );
1052 my $ten_days_ahead = dt_from_string->add( days => 10 );
1054 # Patron is expired and BlockExpiredPatronOpacActions=0
1055 # => auto renew is allowed
1056 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
1057 my $patron = $expired_borrower;
1058 my $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1059 ( $renewokay, $error ) =
1060 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1061 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1062 is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
1063 Koha::Checkouts->find( $checkout->issue_id )->delete;
1066 # Patron is expired and BlockExpiredPatronOpacActions=1
1067 # => auto renew is not allowed
1068 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1069 $patron = $expired_borrower;
1070 $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1071 ( $renewokay, $error ) =
1072 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1073 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1074 is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
1075 Koha::Checkouts->find( $checkout->issue_id )->delete;
1078 # Patron is not expired and BlockExpiredPatronOpacActions=1
1079 # => auto renew is allowed
1080 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1081 $patron = $renewing_borrower;
1082 $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1083 ( $renewokay, $error ) =
1084 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1085 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1086 is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
1087 Koha::Checkouts->find( $checkout->issue_id )->delete;
1090 subtest "GetLatestAutoRenewDate" => sub {
1092 my $item_to_auto_renew = $builder->build_sample_item(
1094 biblionumber => $biblio->biblionumber,
1099 my $ten_days_before = dt_from_string->add( days => -10 );
1100 my $ten_days_ahead = dt_from_string->add( days => 10 );
1101 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1102 Koha::CirculationRules->set_rules(
1104 categorycode => undef,
1105 branchcode => undef,
1108 norenewalbefore => '7',
1109 no_auto_renewal_after => '',
1110 no_auto_renewal_after_hard_limit => undef,
1114 my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1115 is( $latest_auto_renew_date, undef, 'GetLatestAutoRenewDate should return undef if no_auto_renewal_after or no_auto_renewal_after_hard_limit are not defined' );
1116 my $five_days_before = dt_from_string->add( days => -5 );
1117 Koha::CirculationRules->set_rules(
1119 categorycode => undef,
1120 branchcode => undef,
1123 norenewalbefore => '10',
1124 no_auto_renewal_after => '5',
1125 no_auto_renewal_after_hard_limit => undef,
1129 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1130 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1131 $five_days_before->truncate( to => 'minute' ),
1132 'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
1134 my $five_days_ahead = dt_from_string->add( days => 5 );
1135 $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
1136 $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
1137 $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
1138 Koha::CirculationRules->set_rules(
1140 categorycode => undef,
1141 branchcode => undef,
1144 norenewalbefore => '10',
1145 no_auto_renewal_after => '15',
1146 no_auto_renewal_after_hard_limit => undef,
1150 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1151 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1152 $five_days_ahead->truncate( to => 'minute' ),
1153 'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
1155 my $two_days_ahead = dt_from_string->add( days => 2 );
1156 Koha::CirculationRules->set_rules(
1158 categorycode => undef,
1159 branchcode => undef,
1162 norenewalbefore => '10',
1163 no_auto_renewal_after => '',
1164 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1168 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1169 is( $latest_auto_renew_date->truncate( to => 'day' ),
1170 $two_days_ahead->truncate( to => 'day' ),
1171 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
1173 Koha::CirculationRules->set_rules(
1175 categorycode => undef,
1176 branchcode => undef,
1179 norenewalbefore => '10',
1180 no_auto_renewal_after => '15',
1181 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1185 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1186 is( $latest_auto_renew_date->truncate( to => 'day' ),
1187 $two_days_ahead->truncate( to => 'day' ),
1188 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
1194 # set policy to forbid renewals
1195 Koha::CirculationRules->set_rules(
1197 categorycode => undef,
1198 branchcode => undef,
1201 norenewalbefore => undef,
1202 renewalsallowed => 0,
1207 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
1208 is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
1209 is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
1211 # Too many unseen renewals
1212 Koha::CirculationRules->set_rules(
1214 categorycode => undef,
1215 branchcode => undef,
1218 unseen_renewals_allowed => 2,
1219 renewalsallowed => 10,
1223 t::lib::Mocks::mock_preference('UnseenRenewals', 1);
1224 $dbh->do('UPDATE issues SET unseen_renewals = 2 where borrowernumber = ? AND itemnumber = ?', undef, ($renewing_borrowernumber, $item_1->itemnumber));
1225 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
1226 is( $renewokay, 0, 'Cannot renew, 0 unseen renewals allowed');
1227 is( $error, 'too_unseen', 'Cannot renew, returned code is too_unseen');
1228 Koha::CirculationRules->set_rules(
1230 categorycode => undef,
1231 branchcode => undef,
1234 norenewalbefore => undef,
1235 renewalsallowed => 0,
1239 t::lib::Mocks::mock_preference('UnseenRenewals', 0);
1241 # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
1242 t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
1243 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1245 C4::Overdues::UpdateFine(
1247 issue_id => $issue->id(),
1248 itemnumber => $item_1->itemnumber,
1249 borrowernumber => $renewing_borrower->{borrowernumber},
1252 due => Koha::DateUtils::output_pref($datedue)
1256 my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower->{borrowernumber} })->next();
1257 is( $line->debit_type_code, 'OVERDUE', 'Account line type is OVERDUE' );
1258 is( $line->status, 'UNRETURNED', 'Account line status is UNRETURNED' );
1259 is( $line->amountoutstanding+0, 15, 'Account line amount outstanding is 15.00' );
1260 is( $line->amount+0, 15, 'Account line amount is 15.00' );
1261 is( $line->issue_id, $issue->id, 'Account line issue id matches' );
1263 my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
1264 is( $offset->type, 'OVERDUE', 'Account offset type is Fine' );
1265 is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
1267 t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
1268 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
1270 LostItem( $item_1->itemnumber, 'test', 1 );
1272 $line = Koha::Account::Lines->find($line->id);
1273 is( $line->debit_type_code, 'OVERDUE', 'Account type remains as OVERDUE' );
1274 isnt( $line->status, 'UNRETURNED', 'Account status correctly changed from UNRETURNED to RETURNED' );
1276 my $item = Koha::Items->find($item_1->itemnumber);
1277 ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
1278 my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
1279 is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
1281 my $total_due = $dbh->selectrow_array(
1282 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1283 undef, $renewing_borrower->{borrowernumber}
1286 is( $total_due+0, 15, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
1288 C4::Context->dbh->do("DELETE FROM accountlines");
1290 C4::Overdues::UpdateFine(
1292 issue_id => $issue2->id(),
1293 itemnumber => $item_2->itemnumber,
1294 borrowernumber => $renewing_borrower->{borrowernumber},
1297 due => Koha::DateUtils::output_pref($datedue)
1301 LostItem( $item_2->itemnumber, 'test', 0 );
1303 my $item2 = Koha::Items->find($item_2->itemnumber);
1304 ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
1305 ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
1307 $total_due = $dbh->selectrow_array(
1308 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1309 undef, $renewing_borrower->{borrowernumber}
1312 ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
1314 my $future = dt_from_string();
1315 $future->add( days => 7 );
1316 my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
1317 ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
1319 # Users cannot renew any item if there is an overdue item
1320 t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
1321 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
1322 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1323 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
1324 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1326 my $manager = $builder->build_object({ class => "Koha::Patrons" });
1327 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
1328 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1329 $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
1330 LostItem( $item_3->itemnumber, 'test', 0 );
1331 my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
1332 is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
1334 $accountline->description,
1335 sprintf( "%s %s %s",
1336 $item_3->biblio->title || '',
1337 $item_3->barcode || '',
1338 $item_3->itemcallnumber || '' ),
1339 "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1343 subtest "GetUpcomingDueIssues" => sub {
1346 my $branch = $library2->{branchcode};
1348 #Create another record
1349 my $biblio2 = $builder->build_sample_biblio();
1352 my $item_1 = Koha::Items->find($reused_itemnumber_1);
1353 my $item_2 = Koha::Items->find($reused_itemnumber_2);
1354 my $item_3 = $builder->build_sample_item(
1356 biblionumber => $biblio2->biblionumber,
1364 my %a_borrower_data = (
1365 firstname => 'Fridolyn',
1366 surname => 'SOMERS',
1367 categorycode => $patron_category->{categorycode},
1368 branchcode => $branch,
1371 my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1372 my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber )->unblessed;
1374 my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1375 my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1376 my $today = DateTime->today(time_zone => C4::Context->tz());
1378 my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1379 my $datedue = dt_from_string( $issue->date_due() );
1380 my $issue2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1381 my $datedue2 = dt_from_string( $issue->date_due() );
1385 # GetUpcomingDueIssues tests
1387 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1388 is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1391 #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1392 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1393 is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1396 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1397 is ( scalar( @$upcoming_dues ), 1,
1398 "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1401 # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1403 my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1405 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1406 is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1408 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1409 is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1411 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1412 is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1414 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1415 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1417 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1418 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1420 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1421 is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1425 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1426 my $branch = $library2->{branchcode};
1428 my $biblio = $builder->build_sample_biblio();
1431 my $item = $builder->build_sample_item(
1433 biblionumber => $biblio->biblionumber,
1440 my %a_borrower_data = (
1441 firstname => 'Kyle',
1443 categorycode => $patron_category->{categorycode},
1444 branchcode => $branch,
1447 my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1449 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1450 my $issue = AddIssue( $borrower, $item->barcode );
1453 issue_id => $issue->id(),
1454 itemnumber => $item->itemnumber,
1455 borrowernumber => $borrowernumber,
1461 my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1462 my $count = $hr->{count};
1464 is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1467 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1468 $dbh->do('DELETE FROM issues');
1469 $dbh->do('DELETE FROM items');
1470 $dbh->do('DELETE FROM circulation_rules');
1471 Koha::CirculationRules->set_rules(
1473 categorycode => undef,
1475 branchcode => undef,
1477 reservesallowed => 25,
1479 lengthunit => 'days',
1480 renewalsallowed => 1,
1482 norenewalbefore => undef,
1490 my $biblio = $builder->build_sample_biblio();
1492 my $item_1 = $builder->build_sample_item(
1494 biblionumber => $biblio->biblionumber,
1495 library => $library2->{branchcode},
1500 my $item_2= $builder->build_sample_item(
1502 biblionumber => $biblio->biblionumber,
1503 library => $library2->{branchcode},
1508 my $borrowernumber1 = Koha::Patron->new({
1509 firstname => 'Kyle',
1511 categorycode => $patron_category->{categorycode},
1512 branchcode => $library2->{branchcode},
1513 })->store->borrowernumber;
1514 my $borrowernumber2 = Koha::Patron->new({
1515 firstname => 'Chelsea',
1517 categorycode => $patron_category->{categorycode},
1518 branchcode => $library2->{branchcode},
1519 })->store->borrowernumber;
1521 my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
1522 my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
1524 my $issue = AddIssue( $borrower1, $item_1->barcode );
1526 my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1527 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1531 branchcode => $library2->{branchcode},
1532 borrowernumber => $borrowernumber2,
1533 biblionumber => $biblio->biblionumber,
1538 Koha::CirculationRules->set_rules(
1540 categorycode => undef,
1542 branchcode => undef,
1548 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1549 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1550 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1552 Koha::CirculationRules->set_rules(
1554 categorycode => undef,
1556 branchcode => undef,
1562 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1563 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1564 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1566 Koha::CirculationRules->set_rules(
1568 categorycode => undef,
1570 branchcode => undef,
1576 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1577 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1578 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1580 Koha::CirculationRules->set_rules(
1582 categorycode => undef,
1584 branchcode => undef,
1590 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1591 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1592 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1594 # Setting item not checked out to be not for loan but holdable
1595 $item_2->notforloan(-1)->store;
1597 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1598 is( $renewokay, 0, 'Bug 14337 - Verify the borrower can not renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled but the only available item is notforloan' );
1602 # Don't allow renewing onsite checkout
1603 my $branch = $library->{branchcode};
1605 #Create another record
1606 my $biblio = $builder->build_sample_biblio();
1608 my $item = $builder->build_sample_item(
1610 biblionumber => $biblio->biblionumber,
1616 my $borrowernumber = Koha::Patron->new({
1619 categorycode => $patron_category->{categorycode},
1620 branchcode => $branch,
1621 })->store->borrowernumber;
1623 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1625 my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
1626 my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $item->itemnumber );
1627 is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
1628 is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
1632 my $library = $builder->build({ source => 'Branch' });
1634 my $biblio = $builder->build_sample_biblio();
1636 my $item = $builder->build_sample_item(
1638 biblionumber => $biblio->biblionumber,
1639 library => $library->{branchcode},
1644 my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
1646 my $issue = AddIssue( $patron, $item->barcode );
1649 issue_id => $issue->id(),
1650 itemnumber => $item->itemnumber,
1651 borrowernumber => $patron->{borrowernumber},
1658 issue_id => $issue->id(),
1659 itemnumber => $item->itemnumber,
1660 borrowernumber => $patron->{borrowernumber},
1665 is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1668 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1671 my $homebranch = $builder->build( { source => 'Branch' } );
1672 my $holdingbranch = $builder->build( { source => 'Branch' } );
1673 my $otherbranch = $builder->build( { source => 'Branch' } );
1674 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1675 my $patron_2 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1677 my $item = $builder->build_sample_item(
1679 homebranch => $homebranch->{branchcode},
1680 holdingbranch => $holdingbranch->{branchcode},
1684 set_userenv($holdingbranch);
1686 my $issue = AddIssue( $patron_1->unblessed, $item->barcode );
1687 is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
1689 my ( $error, $question, $alerts );
1691 # AllowReturnToBranch == anywhere
1692 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1693 ## Test that unknown barcodes don't generate internal server errors
1694 set_userenv($homebranch);
1695 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
1696 ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
1697 ## Can be issued from homebranch
1698 set_userenv($homebranch);
1699 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1700 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1701 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1702 ## Can be issued from holdingbranch
1703 set_userenv($holdingbranch);
1704 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1705 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1706 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1707 ## Can be issued from another branch
1708 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1709 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1710 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1712 # AllowReturnToBranch == holdingbranch
1713 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1714 ## Cannot be issued from homebranch
1715 set_userenv($homebranch);
1716 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1717 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1718 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1719 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
1720 ## Can be issued from holdinbranch
1721 set_userenv($holdingbranch);
1722 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1723 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1724 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1725 ## Cannot be issued from another branch
1726 set_userenv($otherbranch);
1727 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1728 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1729 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1730 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
1732 # AllowReturnToBranch == homebranch
1733 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1734 ## Can be issued from holdinbranch
1735 set_userenv($homebranch);
1736 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1737 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1738 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1739 ## Cannot be issued from holdinbranch
1740 set_userenv($holdingbranch);
1741 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1742 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1743 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1744 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1745 ## Cannot be issued from holdinbranch
1746 set_userenv($otherbranch);
1747 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1748 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1749 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1750 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1752 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1755 subtest 'AddIssue & AllowReturnToBranch' => sub {
1758 my $homebranch = $builder->build( { source => 'Branch' } );
1759 my $holdingbranch = $builder->build( { source => 'Branch' } );
1760 my $otherbranch = $builder->build( { source => 'Branch' } );
1761 my $patron_1 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1762 my $patron_2 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1764 my $item = $builder->build_sample_item(
1766 homebranch => $homebranch->{branchcode},
1767 holdingbranch => $holdingbranch->{branchcode},
1771 set_userenv($holdingbranch);
1773 my $ref_issue = 'Koha::Checkout';
1774 my $issue = AddIssue( $patron_1, $item->barcode );
1776 my ( $error, $question, $alerts );
1778 # AllowReturnToBranch == homebranch
1779 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1780 ## Can be issued from homebranch
1781 set_userenv($homebranch);
1782 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
1783 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1784 ## Can be issued from holdinbranch
1785 set_userenv($holdingbranch);
1786 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
1787 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1788 ## Can be issued from another branch
1789 set_userenv($otherbranch);
1790 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
1791 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1793 # AllowReturnToBranch == holdinbranch
1794 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1795 ## Cannot be issued from homebranch
1796 set_userenv($homebranch);
1797 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
1798 ## Can be issued from holdingbranch
1799 set_userenv($holdingbranch);
1800 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
1801 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1802 ## Cannot be issued from another branch
1803 set_userenv($otherbranch);
1804 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
1806 # AllowReturnToBranch == homebranch
1807 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1808 ## Can be issued from homebranch
1809 set_userenv($homebranch);
1810 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
1811 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1812 ## Cannot be issued from holdinbranch
1813 set_userenv($holdingbranch);
1814 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
1815 ## Cannot be issued from another branch
1816 set_userenv($otherbranch);
1817 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
1818 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1821 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1824 my $library = $builder->build( { source => 'Branch' } );
1825 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1826 my $item_1 = $builder->build_sample_item(
1828 library => $library->{branchcode},
1831 my $item_2 = $builder->build_sample_item(
1833 library => $library->{branchcode},
1837 my ( $error, $question, $alerts );
1839 # Patron cannot issue item_1, they have overdues
1840 my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1841 my $issue = AddIssue( $patron->unblessed, $item_1->barcode, $yesterday ); # Add an overdue
1843 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1844 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1845 is( keys(%$error) + keys(%$alerts), 0, 'No key for error and alert' . str($error, $question, $alerts) );
1846 is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
1848 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1849 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1850 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1851 is( $error->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
1853 # Patron cannot issue item_1, they are debarred
1854 my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1855 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
1856 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1857 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1858 is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
1860 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
1861 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1862 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1863 is( $error->{USERBLOCKEDNOENDDATE}, '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
1866 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
1869 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1870 my $patron_category_x = $builder->build_object(
1872 class => 'Koha::Patron::Categories',
1873 value => { category_type => 'X' }
1876 my $patron = $builder->build_object(
1878 class => 'Koha::Patrons',
1880 categorycode => $patron_category_x->categorycode,
1881 gonenoaddress => undef,
1888 my $item_1 = $builder->build_sample_item(
1890 library => $library->{branchcode},
1894 my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->barcode );
1895 is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
1897 # TODO There are other tests to provide here
1900 subtest 'MultipleReserves' => sub {
1903 my $biblio = $builder->build_sample_biblio();
1905 my $branch = $library2->{branchcode};
1907 my $item_1 = $builder->build_sample_item(
1909 biblionumber => $biblio->biblionumber,
1911 replacementprice => 12.00,
1916 my $item_2 = $builder->build_sample_item(
1918 biblionumber => $biblio->biblionumber,
1920 replacementprice => 12.00,
1927 my $resdate = undef;
1928 my $expdate = undef;
1930 my $checkitem = undef;
1933 my %renewing_borrower_data = (
1934 firstname => 'John',
1935 surname => 'Renewal',
1936 categorycode => $patron_category->{categorycode},
1937 branchcode => $branch,
1939 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
1940 my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
1941 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
1942 my $datedue = dt_from_string( $issue->date_due() );
1943 is (defined $issue->date_due(), 1, "item 1 checked out");
1944 my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
1946 my %reserving_borrower_data1 = (
1947 firstname => 'Katrin',
1948 surname => 'Reservation',
1949 categorycode => $patron_category->{categorycode},
1950 branchcode => $branch,
1952 my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
1955 branchcode => $branch,
1956 borrowernumber => $reserving_borrowernumber1,
1957 biblionumber => $biblio->biblionumber,
1958 priority => $priority,
1959 reservation_date => $resdate,
1960 expiration_date => $expdate,
1962 itemnumber => $checkitem,
1967 my %reserving_borrower_data2 = (
1968 firstname => 'Kirk',
1969 surname => 'Reservation',
1970 categorycode => $patron_category->{categorycode},
1971 branchcode => $branch,
1973 my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
1976 branchcode => $branch,
1977 borrowernumber => $reserving_borrowernumber2,
1978 biblionumber => $biblio->biblionumber,
1979 priority => $priority,
1980 reservation_date => $resdate,
1981 expiration_date => $expdate,
1983 itemnumber => $checkitem,
1989 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1990 is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
1993 my $item_3 = $builder->build_sample_item(
1995 biblionumber => $biblio->biblionumber,
1997 replacementprice => 12.00,
2003 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
2004 is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
2008 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
2011 my $library = $builder->build( { source => 'Branch' } );
2012 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2014 my $biblionumber = $builder->build_sample_biblio(
2016 branchcode => $library->{branchcode},
2019 my $item_1 = $builder->build_sample_item(
2021 biblionumber => $biblionumber,
2022 library => $library->{branchcode},
2026 my $item_2 = $builder->build_sample_item(
2028 biblionumber => $biblionumber,
2029 library => $library->{branchcode},
2033 my ( $error, $question, $alerts );
2034 my $issue = AddIssue( $patron->unblessed, $item_1->barcode, dt_from_string->add( days => 1 ) );
2036 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2037 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2039 { error => $error, alerts => $alerts },
2040 { error => {}, alerts => {} },
2041 'No error or alert should be raised'
2043 is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
2045 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2046 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2048 { error => $error, question => $question, alerts => $alerts },
2049 { error => {}, question => {}, alerts => {} },
2050 'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
2053 # Add a subscription
2054 Koha::Subscription->new({ biblionumber => $biblionumber })->store;
2056 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2057 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2059 { error => $error, question => $question, alerts => $alerts },
2060 { error => {}, question => {}, alerts => {} },
2061 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2064 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2065 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2067 { error => $error, question => $question, alerts => $alerts },
2068 { error => {}, question => {}, alerts => {} },
2069 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2073 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
2076 my $library = $builder->build( { source => 'Branch' } );
2077 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2080 my $biblionumber = $builder->build_sample_biblio(
2082 branchcode => $library->{branchcode},
2085 my $item_1 = $builder->build_sample_item(
2087 biblionumber => $biblionumber,
2088 library => $library->{branchcode},
2091 my $item_2 = $builder->build_sample_item(
2093 biblionumber => $biblionumber,
2094 library => $library->{branchcode},
2098 # And the circulation rule
2099 Koha::CirculationRules->search->delete;
2100 Koha::CirculationRules->set_rules(
2102 categorycode => undef,
2104 branchcode => undef,
2107 firstremind => 1, # 1 day of grace
2108 finedays => 2, # 2 days of fine per day of overdue
2109 lengthunit => 'days',
2114 # Patron cannot issue item_1, they have overdues
2115 my $now = dt_from_string;
2116 my $five_days_ago = $now->clone->subtract( days => 5 );
2117 my $ten_days_ago = $now->clone->subtract( days => 10 );
2118 AddIssue( $patron, $item_1->barcode, $five_days_ago ); # Add an overdue
2119 AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2120 ; # Add another overdue
2122 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2123 AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2124 my $debarments = Koha::Patron::Debarments::GetDebarments(
2125 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2126 is( scalar(@$debarments), 1 );
2128 # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2129 # Same for the others
2130 my $expected_expiration = output_pref(
2132 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2133 dateformat => 'sql',
2137 is( $debarments->[0]->{expiration}, $expected_expiration );
2139 AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2140 $debarments = Koha::Patron::Debarments::GetDebarments(
2141 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2142 is( scalar(@$debarments), 1 );
2143 $expected_expiration = output_pref(
2145 dt => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2146 dateformat => 'sql',
2150 is( $debarments->[0]->{expiration}, $expected_expiration );
2152 Koha::Patron::Debarments::DelUniqueDebarment(
2153 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2155 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2156 AddIssue( $patron, $item_1->barcode, $five_days_ago ); # Add an overdue
2157 AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2158 ; # Add another overdue
2159 AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2160 $debarments = Koha::Patron::Debarments::GetDebarments(
2161 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2162 is( scalar(@$debarments), 1 );
2163 $expected_expiration = output_pref(
2165 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2166 dateformat => 'sql',
2170 is( $debarments->[0]->{expiration}, $expected_expiration );
2172 AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2173 $debarments = Koha::Patron::Debarments::GetDebarments(
2174 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2175 is( scalar(@$debarments), 1 );
2176 $expected_expiration = output_pref(
2178 dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
2179 dateformat => 'sql',
2183 is( $debarments->[0]->{expiration}, $expected_expiration );
2186 subtest 'AddReturn + suspension_chargeperiod' => sub {
2189 my $library = $builder->build( { source => 'Branch' } );
2190 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2192 my $biblionumber = $builder->build_sample_biblio(
2194 branchcode => $library->{branchcode},
2197 my $item_1 = $builder->build_sample_item(
2199 biblionumber => $biblionumber,
2200 library => $library->{branchcode},
2204 # And the issuing rule
2205 Koha::CirculationRules->search->delete;
2206 Koha::CirculationRules->set_rules(
2208 categorycode => '*',
2213 firstremind => 0, # 0 day of grace
2214 finedays => 2, # 2 days of fine per day of overdue
2215 suspension_chargeperiod => 1,
2216 lengthunit => 'days',
2221 my $now = dt_from_string;
2222 my $five_days_ago = $now->clone->subtract( days => 5 );
2223 # We want to charge 2 days every day, without grace
2224 # With 5 days of overdue: 5 * Z
2225 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2226 test_debarment_on_checkout(
2229 library => $library,
2231 due_date => $five_days_ago,
2232 expiration_date => $expected_expiration,
2236 # Same with undef firstremind
2237 Koha::CirculationRules->search->delete;
2238 Koha::CirculationRules->set_rules(
2240 categorycode => '*',
2245 firstremind => undef, # 0 day of grace
2246 finedays => 2, # 2 days of fine per day of overdue
2247 suspension_chargeperiod => 1,
2248 lengthunit => 'days',
2253 my $now = dt_from_string;
2254 my $five_days_ago = $now->clone->subtract( days => 5 );
2255 # We want to charge 2 days every day, without grace
2256 # With 5 days of overdue: 5 * Z
2257 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2258 test_debarment_on_checkout(
2261 library => $library,
2263 due_date => $five_days_ago,
2264 expiration_date => $expected_expiration,
2268 # We want to charge 2 days every 2 days, without grace
2269 # With 5 days of overdue: (5 * 2) / 2
2270 Koha::CirculationRules->set_rule(
2272 categorycode => undef,
2273 branchcode => undef,
2275 rule_name => 'suspension_chargeperiod',
2280 $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
2281 test_debarment_on_checkout(
2284 library => $library,
2286 due_date => $five_days_ago,
2287 expiration_date => $expected_expiration,
2291 # We want to charge 2 days every 3 days, with 1 day of grace
2292 # With 5 days of overdue: ((5-1) / 3 ) * 2
2293 Koha::CirculationRules->set_rules(
2295 categorycode => undef,
2296 branchcode => undef,
2299 suspension_chargeperiod => 3,
2304 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
2305 test_debarment_on_checkout(
2308 library => $library,
2310 due_date => $five_days_ago,
2311 expiration_date => $expected_expiration,
2315 # Use finesCalendar to know if holiday must be skipped to calculate the due date
2316 # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
2317 Koha::CirculationRules->set_rules(
2319 categorycode => undef,
2320 branchcode => undef,
2324 suspension_chargeperiod => 1,
2329 t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
2330 t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
2332 # Adding a holiday 2 days ago
2333 my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
2334 my $two_days_ago = $now->clone->subtract( days => 2 );
2335 $calendar->insert_single_holiday(
2336 day => $two_days_ago->day,
2337 month => $two_days_ago->month,
2338 year => $two_days_ago->year,
2339 title => 'holidayTest-2d',
2340 description => 'holidayDesc 2 days ago'
2342 # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
2343 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
2344 test_debarment_on_checkout(
2347 library => $library,
2349 due_date => $five_days_ago,
2350 expiration_date => $expected_expiration,
2354 # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
2355 my $two_days_ahead = $now->clone->add( days => 2 );
2356 $calendar->insert_single_holiday(
2357 day => $two_days_ahead->day,
2358 month => $two_days_ahead->month,
2359 year => $two_days_ahead->year,
2360 title => 'holidayTest+2d',
2361 description => 'holidayDesc 2 days ahead'
2364 # Same as above, but we should skip D+2
2365 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
2366 test_debarment_on_checkout(
2369 library => $library,
2371 due_date => $five_days_ago,
2372 expiration_date => $expected_expiration,
2376 # Adding another holiday, day of expiration date
2377 my $expected_expiration_dt = dt_from_string($expected_expiration);
2378 $calendar->insert_single_holiday(
2379 day => $expected_expiration_dt->day,
2380 month => $expected_expiration_dt->month,
2381 year => $expected_expiration_dt->year,
2382 title => 'holidayTest_exp',
2383 description => 'holidayDesc on expiration date'
2385 # Expiration date will be the day after
2386 test_debarment_on_checkout(
2389 library => $library,
2391 due_date => $five_days_ago,
2392 expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
2396 test_debarment_on_checkout(
2399 library => $library,
2401 return_date => $now->clone->add(days => 5),
2402 expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
2406 test_debarment_on_checkout(
2409 library => $library,
2411 due_date => $now->clone->add(days => 1),
2412 return_date => $now->clone->add(days => 5),
2413 expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
2419 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
2422 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2423 my $patron1 = $builder->build_object(
2425 class => 'Koha::Patrons',
2427 library => $library->branchcode,
2428 categorycode => $patron_category->{categorycode}
2432 my $patron2 = $builder->build_object(
2434 class => 'Koha::Patrons',
2436 library => $library->branchcode,
2437 categorycode => $patron_category->{categorycode}
2442 t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
2444 my $item = $builder->build_sample_item(
2446 library => $library->branchcode,
2450 my ( $error, $question, $alerts );
2451 my $issue = AddIssue( $patron1->unblessed, $item->barcode );
2453 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2454 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
2455 is( $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER question flag should be set if AutoReturnCheckedOutItems is disabled and item is checked out to another' );
2457 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
2458 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
2459 is( $alerts->{RETURNED_FROM_ANOTHER}->{patron}->borrowernumber, $patron1->borrowernumber, 'RETURNED_FROM_ANOTHER alert flag should be set if AutoReturnCheckedOutItems is enabled and item is checked out to another' );
2461 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2465 subtest 'AddReturn | is_overdue' => sub {
2468 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
2469 t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
2470 t::lib::Mocks::mock_preference('finesMode', 'production');
2471 t::lib::Mocks::mock_preference('MaxFine', '100');
2473 my $library = $builder->build( { source => 'Branch' } );
2474 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2475 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2476 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
2478 my $item = $builder->build_sample_item(
2480 library => $library->{branchcode},
2481 replacementprice => 7
2485 Koha::CirculationRules->search->delete;
2486 Koha::CirculationRules->set_rules(
2488 categorycode => undef,
2490 branchcode => undef,
2493 lengthunit => 'days',
2494 fine => 1, # Charge 1 every day of overdue
2500 my $now = dt_from_string;
2501 my $one_day_ago = $now->clone->subtract( days => 1 );
2502 my $two_days_ago = $now->clone->subtract( days => 2 );
2503 my $five_days_ago = $now->clone->subtract( days => 5 );
2504 my $ten_days_ago = $now->clone->subtract( days => 10 );
2505 $patron = Koha::Patrons->find( $patron->{borrowernumber} );
2507 # No return date specified, today will be used => 10 days overdue charged
2508 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2509 AddReturn( $item->barcode, $library->{branchcode} );
2510 is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
2511 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2513 # specify return date 5 days before => no overdue charged
2514 AddIssue( $patron->unblessed, $item->barcode, $five_days_ago ); # date due was 5d ago
2515 AddReturn( $item->barcode, $library->{branchcode}, undef, $ten_days_ago );
2516 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2517 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2519 # specify return date 5 days later => 5 days overdue charged
2520 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2521 AddReturn( $item->barcode, $library->{branchcode}, undef, $five_days_ago );
2522 is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
2523 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2525 # specify return date 5 days later, specify exemptfine => no overdue charge
2526 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2527 AddReturn( $item->barcode, $library->{branchcode}, 1, $five_days_ago );
2528 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2529 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2531 subtest 'bug 22877 | Lost item return' => sub {
2535 my $issue = AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2537 # Fake fines cronjob on this checkout
2539 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2540 $ten_days_ago, $now );
2543 issue_id => $issue->issue_id,
2544 itemnumber => $item->itemnumber,
2545 borrowernumber => $patron->borrowernumber,
2547 due => output_pref($ten_days_ago)
2550 is( int( $patron->account->balance() ),
2551 10, "Overdue fine of 10 days overdue" );
2553 # Fake longoverdue with charge and not marking returned
2554 LostItem( $item->itemnumber, 'cronjob', 0 );
2555 is( int( $patron->account->balance() ),
2556 17, "Lost fine of 7 plus 10 days overdue" );
2558 # Now we return it today
2559 AddReturn( $item->barcode, $library->{branchcode} );
2560 is( int( $patron->account->balance() ),
2561 17, "Should have a single 10 days overdue fine and lost charge" );
2564 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2567 subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
2571 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2573 my $issue = AddIssue( $patron->unblessed, $item->barcode, $one_day_ago ); # date due was 1d ago
2575 # Fake fines cronjob on this checkout
2577 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2578 $one_day_ago, $now );
2581 issue_id => $issue->issue_id,
2582 itemnumber => $item->itemnumber,
2583 borrowernumber => $patron->borrowernumber,
2585 due => output_pref($one_day_ago)
2588 is( int( $patron->account->balance() ),
2589 1, "Overdue fine of 1 day overdue" );
2591 # Backdated return (dropbox mode example - charge should be removed)
2592 AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
2593 is( int( $patron->account->balance() ),
2594 0, "Overdue fine should be annulled" );
2595 my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2596 is( $lines->count, 0, "Overdue fine accountline has been removed");
2598 $issue = AddIssue( $patron->unblessed, $item->barcode, $two_days_ago ); # date due was 2d ago
2600 # Fake fines cronjob on this checkout
2602 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2603 $two_days_ago, $now );
2606 issue_id => $issue->issue_id,
2607 itemnumber => $item->itemnumber,
2608 borrowernumber => $patron->borrowernumber,
2610 due => output_pref($one_day_ago)
2613 is( int( $patron->account->balance() ),
2614 2, "Overdue fine of 2 days overdue" );
2616 # Payment made against fine
2617 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2618 my $debit = $lines->next;
2619 my $credit = $patron->account->add_credit(
2623 interface => 'test',
2627 { debits => [ $debit ], offset_type => 'Payment' } );
2629 is( int( $patron->account->balance() ),
2630 0, "Overdue fine should be paid off" );
2631 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2632 is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
2633 my $line = $lines->next;
2634 is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
2635 is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
2637 # Backdated return (dropbox mode example - charge should be removed)
2638 AddReturn( $item->barcode, $library->{branchcode}, undef, $one_day_ago );
2639 is( int( $patron->account->balance() ),
2640 -1, "Refund credit has been applied" );
2641 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
2642 is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
2644 $line = $lines->next;
2645 is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
2646 is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
2647 is($line->status,'RETURNED', "Overdue fine is fixed");
2648 $line = $lines->next;
2649 is($line->amount+0,-2, "Original payment amount remains as 2");
2650 is($line->amountoutstanding+0,0, "Original payment remains applied");
2651 $line = $lines->next;
2652 is($line->amount+0,-1, "Refund amount correctly set to 1");
2653 is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
2656 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2659 subtest 'bug 25417 | backdated return + exemptfine' => sub {
2663 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2665 my $issue = AddIssue( $patron->unblessed, $item->barcode, $one_day_ago ); # date due was 1d ago
2667 # Fake fines cronjob on this checkout
2669 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2670 $one_day_ago, $now );
2673 issue_id => $issue->issue_id,
2674 itemnumber => $item->itemnumber,
2675 borrowernumber => $patron->borrowernumber,
2677 due => output_pref($one_day_ago)
2680 is( int( $patron->account->balance() ),
2681 1, "Overdue fine of 1 day overdue" );
2683 # Backdated return (dropbox mode example - charge should no longer exist)
2684 AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
2685 is( int( $patron->account->balance() ),
2686 0, "Overdue fine should be annulled" );
2689 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2692 subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
2695 t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
2697 my $due_date = dt_from_string;
2698 my $issue = AddIssue( $patron->unblessed, $item->barcode, $due_date );
2703 issue_id => $issue->issue_id,
2704 itemnumber => $item->itemnumber,
2705 borrowernumber => $patron->borrowernumber,
2707 due => output_pref($due_date)
2710 is( $patron->account->balance(),
2711 0.25, 'Overdue fine of $0.25 recorded' );
2713 # Backdate return to exact due date and time
2714 my ( undef, $message ) =
2715 AddReturn( $item->barcode, $library->{branchcode},
2719 Koha::Account::Lines->find( { issue_id => $issue->id } );
2720 ok( !$accountline, 'accountline removed as expected' );
2723 $issue = AddIssue( $patron->unblessed, $item->barcode, $due_date );
2728 issue_id => $issue->issue_id,
2729 itemnumber => $item->itemnumber,
2730 borrowernumber => $patron->borrowernumber,
2732 due => output_pref($due_date)
2735 is( $patron->account->balance(),
2736 0.25, 'Overdue fine of $0.25 recorded' );
2738 # Partial pay accruing fine
2739 my $lines = Koha::Account::Lines->search(
2741 borrowernumber => $patron->borrowernumber,
2742 issue_id => $issue->id
2745 my $debit = $lines->next;
2746 my $credit = $patron->account->add_credit(
2750 interface => 'test',
2753 $credit->apply( { debits => [$debit], offset_type => 'Payment' } );
2755 is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
2757 # Backdate return to exact due date and time
2758 ( undef, $message ) =
2759 AddReturn( $item->barcode, $library->{branchcode},
2762 $lines = Koha::Account::Lines->search(
2764 borrowernumber => $patron->borrowernumber,
2765 issue_id => $issue->id
2768 $accountline = $lines->next;
2769 is( $accountline->amountoutstanding + 0,
2770 0, 'Partially paid fee amount outstanding was reduced to 0' );
2771 is( $accountline->amount + 0,
2772 0, 'Partially paid fee amount was reduced to 0' );
2773 is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
2776 Koha::Account::Lines->search(
2777 { borrowernumber => $patron->borrowernumber } )->delete;
2780 subtest 'enh 23091 | Lost item return policies' => sub {
2783 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2785 my $branchcode_false =
2786 $builder->build( { source => 'Branch' } )->{branchcode};
2787 my $specific_rule_false = $builder->build(
2789 source => 'CirculationRule',
2791 branchcode => $branchcode_false,
2792 categorycode => undef,
2794 rule_name => 'lostreturn',
2799 my $branchcode_refund =
2800 $builder->build( { source => 'Branch' } )->{branchcode};
2801 my $specific_rule_refund = $builder->build(
2803 source => 'CirculationRule',
2805 branchcode => $branchcode_refund,
2806 categorycode => undef,
2808 rule_name => 'lostreturn',
2809 rule_value => 'refund'
2813 my $branchcode_restore =
2814 $builder->build( { source => 'Branch' } )->{branchcode};
2815 my $specific_rule_restore = $builder->build(
2817 source => 'CirculationRule',
2819 branchcode => $branchcode_restore,
2820 categorycode => undef,
2822 rule_name => 'lostreturn',
2823 rule_value => 'restore'
2827 my $branchcode_charge =
2828 $builder->build( { source => 'Branch' } )->{branchcode};
2829 my $specific_rule_charge = $builder->build(
2831 source => 'CirculationRule',
2833 branchcode => $branchcode_charge,
2834 categorycode => undef,
2836 rule_name => 'lostreturn',
2837 rule_value => 'charge'
2842 my $replacement_amount = 99.00;
2843 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2844 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
2845 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
2846 t::lib::Mocks::mock_preference( 'BlockReturnOfLostItems', 0 );
2847 t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl',
2849 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge',
2852 subtest 'lostreturn | false' => sub {
2855 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_false });
2857 my $item = $builder->build_sample_item(
2859 replacementprice => $replacement_amount
2864 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
2866 # Fake fines cronjob on this checkout
2868 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2869 $ten_days_ago, $now );
2872 issue_id => $issue->issue_id,
2873 itemnumber => $item->itemnumber,
2874 borrowernumber => $patron->borrowernumber,
2876 due => output_pref($ten_days_ago)
2879 my $overdue_fees = Koha::Account::Lines->search(
2881 borrowernumber => $patron->id,
2882 itemnumber => $item->itemnumber,
2883 debit_type_code => 'OVERDUE'
2886 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
2887 my $overdue_fee = $overdue_fees->next;
2888 is( $overdue_fee->amount + 0,
2889 10, 'The right OVERDUE amount is generated' );
2890 is( $overdue_fee->amountoutstanding + 0,
2892 'The right OVERDUE amountoutstanding is generated' );
2894 # Simulate item marked as lost
2895 $item->itemlost(3)->store;
2896 C4::Circulation::LostItem( $item->itemnumber, 1 );
2898 my $lost_fee_lines = Koha::Account::Lines->search(
2900 borrowernumber => $patron->id,
2901 itemnumber => $item->itemnumber,
2902 debit_type_code => 'LOST'
2905 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
2906 my $lost_fee_line = $lost_fee_lines->next;
2907 is( $lost_fee_line->amount + 0,
2908 $replacement_amount, 'The right LOST amount is generated' );
2909 is( $lost_fee_line->amountoutstanding + 0,
2910 $replacement_amount,
2911 'The right LOST amountoutstanding is generated' );
2912 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2915 my ( $returned, $message ) =
2916 AddReturn( $item->barcode, $branchcode_false, undef, $five_days_ago );
2918 $overdue_fee->discard_changes;
2919 is( $overdue_fee->amount + 0,
2920 10, 'The OVERDUE amount is left intact' );
2921 is( $overdue_fee->amountoutstanding + 0,
2923 'The OVERDUE amountoutstanding is left intact' );
2925 $lost_fee_line->discard_changes;
2926 is( $lost_fee_line->amount + 0,
2927 $replacement_amount, 'The LOST amount is left intact' );
2928 is( $lost_fee_line->amountoutstanding + 0,
2929 $replacement_amount,
2930 'The LOST amountoutstanding is left intact' );
2931 # FIXME: Should we set the LOST fee status to 'FOUND' regardless of whether we're refunding or not?
2932 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2935 subtest 'lostreturn | refund' => sub {
2938 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund });
2940 my $item = $builder->build_sample_item(
2942 replacementprice => $replacement_amount
2947 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
2949 # Fake fines cronjob on this checkout
2951 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2952 $ten_days_ago, $now );
2955 issue_id => $issue->issue_id,
2956 itemnumber => $item->itemnumber,
2957 borrowernumber => $patron->borrowernumber,
2959 due => output_pref($ten_days_ago)
2962 my $overdue_fees = Koha::Account::Lines->search(
2964 borrowernumber => $patron->id,
2965 itemnumber => $item->itemnumber,
2966 debit_type_code => 'OVERDUE'
2969 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
2970 my $overdue_fee = $overdue_fees->next;
2971 is( $overdue_fee->amount + 0,
2972 10, 'The right OVERDUE amount is generated' );
2973 is( $overdue_fee->amountoutstanding + 0,
2975 'The right OVERDUE amountoutstanding is generated' );
2977 # Simulate item marked as lost
2978 $item->itemlost(3)->store;
2979 C4::Circulation::LostItem( $item->itemnumber, 1 );
2981 my $lost_fee_lines = Koha::Account::Lines->search(
2983 borrowernumber => $patron->id,
2984 itemnumber => $item->itemnumber,
2985 debit_type_code => 'LOST'
2988 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
2989 my $lost_fee_line = $lost_fee_lines->next;
2990 is( $lost_fee_line->amount + 0,
2991 $replacement_amount, 'The right LOST amount is generated' );
2992 is( $lost_fee_line->amountoutstanding + 0,
2993 $replacement_amount,
2994 'The right LOST amountoutstanding is generated' );
2995 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2997 # Return the lost item
2998 my ( undef, $message ) =
2999 AddReturn( $item->barcode, $branchcode_refund, undef, $five_days_ago );
3001 $overdue_fee->discard_changes;
3002 is( $overdue_fee->amount + 0,
3003 10, 'The OVERDUE amount is left intact' );
3004 is( $overdue_fee->amountoutstanding + 0,
3006 'The OVERDUE amountoutstanding is left intact' );
3008 $lost_fee_line->discard_changes;
3009 is( $lost_fee_line->amount + 0,
3010 $replacement_amount, 'The LOST amount is left intact' );
3011 is( $lost_fee_line->amountoutstanding + 0,
3013 'The LOST amountoutstanding is refunded' );
3014 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3017 subtest 'lostreturn | restore' => sub {
3020 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_restore });
3022 my $item = $builder->build_sample_item(
3024 replacementprice => $replacement_amount
3029 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode , $ten_days_ago);
3031 # Fake fines cronjob on this checkout
3033 CalcFine( $item, $patron->categorycode, $library->{branchcode},
3034 $ten_days_ago, $now );
3037 issue_id => $issue->issue_id,
3038 itemnumber => $item->itemnumber,
3039 borrowernumber => $patron->borrowernumber,
3041 due => output_pref($ten_days_ago)
3044 my $overdue_fees = Koha::Account::Lines->search(
3046 borrowernumber => $patron->id,
3047 itemnumber => $item->itemnumber,
3048 debit_type_code => 'OVERDUE'
3051 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3052 my $overdue_fee = $overdue_fees->next;
3053 is( $overdue_fee->amount + 0,
3054 10, 'The right OVERDUE amount is generated' );
3055 is( $overdue_fee->amountoutstanding + 0,
3057 'The right OVERDUE amountoutstanding is generated' );
3059 # Simulate item marked as lost
3060 $item->itemlost(3)->store;
3061 C4::Circulation::LostItem( $item->itemnumber, 1 );
3063 my $lost_fee_lines = Koha::Account::Lines->search(
3065 borrowernumber => $patron->id,
3066 itemnumber => $item->itemnumber,
3067 debit_type_code => 'LOST'
3070 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3071 my $lost_fee_line = $lost_fee_lines->next;
3072 is( $lost_fee_line->amount + 0,
3073 $replacement_amount, 'The right LOST amount is generated' );
3074 is( $lost_fee_line->amountoutstanding + 0,
3075 $replacement_amount,
3076 'The right LOST amountoutstanding is generated' );
3077 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3079 # Simulate refunding overdue fees upon marking item as lost
3080 my $overdue_forgive = $patron->account->add_credit(
3083 user_id => $manager->borrowernumber,
3084 library_id => $branchcode_restore,
3085 interface => 'test',
3087 item_id => $item->itemnumber
3090 $overdue_forgive->apply(
3091 { debits => [$overdue_fee], offset_type => 'Forgiven' } );
3092 $overdue_fee->discard_changes;
3093 is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
3096 my ( undef, $message ) =
3097 AddReturn( $item->barcode, $branchcode_restore, undef, $five_days_ago );
3099 $overdue_fee->discard_changes;
3100 is( $overdue_fee->amount + 0,
3101 10, 'The OVERDUE amount is left intact' );
3102 is( $overdue_fee->amountoutstanding + 0,
3104 'The OVERDUE amountoutstanding is restored' );
3106 $lost_fee_line->discard_changes;
3107 is( $lost_fee_line->amount + 0,
3108 $replacement_amount, 'The LOST amount is left intact' );
3109 is( $lost_fee_line->amountoutstanding + 0,
3111 'The LOST amountoutstanding is refunded' );
3112 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3115 subtest 'lostreturn | charge' => sub {
3118 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_charge });
3120 my $item = $builder->build_sample_item(
3122 replacementprice => $replacement_amount
3127 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
3129 # Fake fines cronjob on this checkout
3131 CalcFine( $item, $patron->categorycode, $library->{branchcode},
3132 $ten_days_ago, $now );
3135 issue_id => $issue->issue_id,
3136 itemnumber => $item->itemnumber,
3137 borrowernumber => $patron->borrowernumber,
3139 due => output_pref($ten_days_ago)
3142 my $overdue_fees = Koha::Account::Lines->search(
3144 borrowernumber => $patron->id,
3145 itemnumber => $item->itemnumber,
3146 debit_type_code => 'OVERDUE'
3149 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3150 my $overdue_fee = $overdue_fees->next;
3151 is( $overdue_fee->amount + 0,
3152 10, 'The right OVERDUE amount is generated' );
3153 is( $overdue_fee->amountoutstanding + 0,
3155 'The right OVERDUE amountoutstanding is generated' );
3157 # Simulate item marked as lost
3158 $item->itemlost(3)->store;
3159 C4::Circulation::LostItem( $item->itemnumber, 1 );
3161 my $lost_fee_lines = Koha::Account::Lines->search(
3163 borrowernumber => $patron->id,
3164 itemnumber => $item->itemnumber,
3165 debit_type_code => 'LOST'
3168 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3169 my $lost_fee_line = $lost_fee_lines->next;
3170 is( $lost_fee_line->amount + 0,
3171 $replacement_amount, 'The right LOST amount is generated' );
3172 is( $lost_fee_line->amountoutstanding + 0,
3173 $replacement_amount,
3174 'The right LOST amountoutstanding is generated' );
3175 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3177 # Simulate refunding overdue fees upon marking item as lost
3178 my $overdue_forgive = $patron->account->add_credit(
3181 user_id => $manager->borrowernumber,
3182 library_id => $branchcode_charge,
3183 interface => 'test',
3185 item_id => $item->itemnumber
3188 $overdue_forgive->apply(
3189 { debits => [$overdue_fee], offset_type => 'Forgiven' } );
3190 $overdue_fee->discard_changes;
3191 is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
3194 my ( undef, $message ) =
3195 AddReturn( $item->barcode, $branchcode_charge, undef, $five_days_ago );
3197 $lost_fee_line->discard_changes;
3198 is( $lost_fee_line->amount + 0,
3199 $replacement_amount, 'The LOST amount is left intact' );
3200 is( $lost_fee_line->amountoutstanding + 0,
3202 'The LOST amountoutstanding is refunded' );
3203 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3205 $overdue_fees = Koha::Account::Lines->search(
3207 borrowernumber => $patron->id,
3208 itemnumber => $item->itemnumber,
3209 debit_type_code => 'OVERDUE'
3212 order_by => { '-asc' => 'accountlines_id'}
3215 is( $overdue_fees->count, 2, 'A second OVERDUE fee has been added' );
3216 $overdue_fee = $overdue_fees->next;
3217 is( $overdue_fee->amount + 0,
3218 10, 'The original OVERDUE amount is left intact' );
3219 is( $overdue_fee->amountoutstanding + 0,
3221 'The original OVERDUE amountoutstanding is left as forgiven' );
3222 $overdue_fee = $overdue_fees->next;
3223 is( $overdue_fee->amount + 0,
3224 5, 'The new OVERDUE amount is correct for the backdated return' );
3225 is( $overdue_fee->amountoutstanding + 0,
3227 'The new OVERDUE amountoutstanding is correct for the backdated return' );
3232 subtest '_FixOverduesOnReturn' => sub {
3235 my $manager = $builder->build_object({ class => "Koha::Patrons" });
3236 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3238 my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
3240 my $branchcode = $library2->{branchcode};
3242 my $item = $builder->build_sample_item(
3244 biblionumber => $biblio->biblionumber,
3245 library => $branchcode,
3246 replacementprice => 99.00,
3251 my $patron = $builder->build( { source => 'Borrower' } );
3253 ## Start with basic call, should just close out the open fine
3254 my $accountline = Koha::Account::Line->new(
3256 borrowernumber => $patron->{borrowernumber},
3257 debit_type_code => 'OVERDUE',
3258 status => 'UNRETURNED',
3259 itemnumber => $item->itemnumber,
3261 amountoutstanding => 99.00,
3262 interface => 'test',
3266 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
3268 $accountline->_result()->discard_changes();
3270 is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
3271 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3272 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3274 ## Run again, with exemptfine enabled
3277 debit_type_code => 'OVERDUE',
3278 status => 'UNRETURNED',
3279 amountoutstanding => 99.00,
3283 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3285 $accountline->_result()->discard_changes();
3286 my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3288 is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
3289 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3290 is( $accountline->status, 'RETURNED', 'Open fine ( account type OVERDUE ) has been set to returned ( status RETURNED )');
3291 is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
3292 is( $offset->amount + 0, -99, "Amount of offset is correct" );
3293 my $credit = $offset->credit;
3294 is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
3295 is( $credit->amount + 0, -99, "Credit amount is set correctly" );
3296 is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
3298 # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
3301 debit_type_code => 'OVERDUE',
3302 status => 'UNRETURNED',
3303 amountoutstanding => 0.00,
3308 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3310 $accountline->_result()->discard_changes();
3311 $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3312 is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
3313 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3314 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3317 subtest 'Set waiting flag' => sub {
3320 my $library_1 = $builder->build( { source => 'Branch' } );
3321 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3322 my $library_2 = $builder->build( { source => 'Branch' } );
3323 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3325 my $item = $builder->build_sample_item(
3327 library => $library_1->{branchcode},
3331 set_userenv( $library_2 );
3332 my $reserve_id = AddReserve(
3334 branchcode => $library_2->{branchcode},
3335 borrowernumber => $patron_2->{borrowernumber},
3336 biblionumber => $item->biblionumber,
3338 itemnumber => $item->itemnumber,
3342 set_userenv( $library_1 );
3343 my $do_transfer = 1;
3344 my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3345 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3346 my $hold = Koha::Holds->find( $reserve_id );
3347 is( $hold->found, 'T', 'Hold is in transit' );
3349 my ( $status ) = CheckReserves($item->itemnumber);
3350 is( $status, 'Reserved', 'Hold is not waiting yet');
3352 set_userenv( $library_2 );
3354 AddReturn( $item->barcode, $library_2->{branchcode} );
3355 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3356 $hold = Koha::Holds->find( $reserve_id );
3357 is( $hold->found, 'W', 'Hold is waiting' );
3358 ( $status ) = CheckReserves($item->itemnumber);
3359 is( $status, 'Waiting', 'Now the hold is waiting');
3361 #Bug 21944 - Waiting transfer checked in at branch other than pickup location
3362 set_userenv( $library_1 );
3363 (undef, my $messages, undef, undef ) = AddReturn ( $item->barcode, $library_1->{branchcode} );
3364 $hold = Koha::Holds->find( $reserve_id );
3365 is( $hold->found, undef, 'Hold is no longer marked waiting' );
3366 is( $hold->priority, 1, "Hold is now priority one again");
3367 is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
3368 is( $hold->itemnumber, $item->itemnumber, "Hold has retained its' itemnumber");
3369 is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
3370 is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
3371 is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
3374 subtest 'Cancel transfers on lost items' => sub {
3376 my $library_1 = $builder->build( { source => 'Branch' } );
3377 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3378 my $library_2 = $builder->build( { source => 'Branch' } );
3379 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3380 my $biblio = $builder->build_sample_biblio({branchcode => $library->{branchcode}});
3381 my $item = $builder->build_sample_item({
3382 biblionumber => $biblio->biblionumber,
3383 library => $library_1->{branchcode},
3386 set_userenv( $library_2 );
3387 my $reserve_id = AddReserve(
3389 branchcode => $library_2->{branchcode},
3390 borrowernumber => $patron_2->{borrowernumber},
3391 biblionumber => $item->biblionumber,
3393 itemnumber => $item->itemnumber,
3397 #Return book and add transfer
3398 set_userenv( $library_1 );
3399 my $do_transfer = 1;
3400 my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3401 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3402 C4::Circulation::transferbook({
3403 from_branch => $library_1->{branchcode},
3404 to_branch => $library_2->{branchcode},
3405 barcode => $item->barcode,
3407 my $hold = Koha::Holds->find( $reserve_id );
3408 is( $hold->found, 'T', 'Hold is in transit' );
3410 #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
3411 my ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3412 is( $frombranch, $library_1->{branchcode}, 'The transfer is generated from the correct library');
3413 is( $tobranch, $library_2->{branchcode}, 'The transfer is generated to the correct library');
3414 my $itemcheck = Koha::Items->find($item->itemnumber);
3415 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Items holding branch is the transfers origination branch before it is marked as lost' );
3417 #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
3418 $item->itemlost(1)->store;
3419 LostItem( $item->itemnumber, 'test', 1 );
3420 ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3421 is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
3422 $itemcheck = Koha::Items->find($item->itemnumber);
3423 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
3427 subtest 'CanBookBeIssued | is_overdue' => sub {
3430 # Set a simple circ policy
3431 Koha::CirculationRules->set_rules(
3433 categorycode => undef,
3434 branchcode => undef,
3438 reservesallowed => 25,
3440 lengthunit => 'days',
3441 renewalsallowed => 1,
3443 norenewalbefore => undef,
3451 my $now = dt_from_string;
3452 my $five_days_go = output_pref({ dt => $now->clone->add( days => 5 ), dateonly => 1});
3453 my $ten_days_go = output_pref({ dt => $now->clone->add( days => 10), dateonly => 1 });
3454 my $library = $builder->build( { source => 'Branch' } );
3455 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
3457 my $item = $builder->build_sample_item(
3459 library => $library->{branchcode},
3463 my $issue = AddIssue( $patron->unblessed, $item->barcode, $five_days_go ); # date due was 10d ago
3464 my $actualissue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
3465 is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
3466 my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->barcode,$ten_days_go, undef, undef, undef);
3467 is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
3468 is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
3471 subtest 'ItemsDeniedRenewal preference' => sub {
3474 C4::Context->set_preference('ItemsDeniedRenewal','');
3476 my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
3477 Koha::CirculationRules->set_rules(
3479 categorycode => '*',
3481 branchcode => $idr_lib->branchcode,
3483 reservesallowed => 25,
3485 lengthunit => 'days',
3486 renewalsallowed => 10,
3488 norenewalbefore => undef,
3496 my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
3497 homebranch => $idr_lib->branchcode,
3501 itemcallnumber => undef,
3505 my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
3506 homebranch => $idr_lib->branchcode,
3509 location => 'NOPROC'
3513 my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
3514 branchcode => $idr_lib->branchcode,
3517 my $future = dt_from_string->add( days => 1 );
3518 my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3519 returndate => undef,
3522 borrowernumber => $idr_borrower->borrowernumber,
3523 itemnumber => $deny_book->itemnumber,
3524 onsite_checkout => 0,
3525 date_due => $future,
3528 my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3529 returndate => undef,
3532 borrowernumber => $idr_borrower->borrowernumber,
3533 itemnumber => $allow_book->itemnumber,
3534 onsite_checkout => 0,
3535 date_due => $future,
3541 my ( $idr_mayrenew, $idr_error ) =
3542 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3543 is( $idr_mayrenew, 1, 'Renewal allowed when no rules' );
3544 is( $idr_error, undef, 'Renewal allowed when no rules' );
3546 $idr_rules="withdrawn: [1]";
3548 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3549 ( $idr_mayrenew, $idr_error ) =
3550 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3551 is( $idr_mayrenew, 0, 'Renewal blocked when 1 rules (withdrawn)' );
3552 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 1 rule (withdrawn)' );
3553 ( $idr_mayrenew, $idr_error ) =
3554 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3555 is( $idr_mayrenew, 1, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3556 is( $idr_error, undef, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3558 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
3560 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3561 ( $idr_mayrenew, $idr_error ) =
3562 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3563 is( $idr_mayrenew, 0, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
3564 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 2 rules matched (withdrawn,itype)' );
3565 ( $idr_mayrenew, $idr_error ) =
3566 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3567 is( $idr_mayrenew, 1, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3568 is( $idr_error, undef, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3570 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
3572 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3573 ( $idr_mayrenew, $idr_error ) =
3574 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3575 is( $idr_mayrenew, 0, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
3576 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 3 rules matched (withdrawn,itype, location)' );
3577 ( $idr_mayrenew, $idr_error ) =
3578 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3579 is( $idr_mayrenew, 1, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3580 is( $idr_error, undef, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3582 $idr_rules="itemcallnumber: [NULL]";
3583 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3584 ( $idr_mayrenew, $idr_error ) =
3585 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3586 is( $idr_mayrenew, 0, 'Renewal blocked for undef when NULL in pref' );
3587 $idr_rules="itemcallnumber: ['']";
3588 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3589 ( $idr_mayrenew, $idr_error ) =
3590 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3591 is( $idr_mayrenew, 1, 'Renewal not blocked for undef when "" in pref' );
3593 $idr_rules="itemnotes: [NULL]";
3594 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3595 ( $idr_mayrenew, $idr_error ) =
3596 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3597 is( $idr_mayrenew, 1, 'Renewal not blocked for "" when NULL in pref' );
3598 $idr_rules="itemnotes: ['']";
3599 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3600 ( $idr_mayrenew, $idr_error ) =
3601 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3602 is( $idr_mayrenew, 0, 'Renewal blocked for empty string when "" in pref' );
3605 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
3608 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3609 my $library = $builder->build( { source => 'Branch' } );
3610 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3612 my $item = $builder->build_sample_item(
3614 library => $library->{branchcode},
3618 my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3619 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3620 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3623 subtest 'CanBookBeIssued | notforloan' => sub {
3626 t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
3628 my $library = $builder->build( { source => 'Branch' } );
3629 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3631 my $itemtype = $builder->build(
3633 source => 'Itemtype',
3634 value => { notforloan => undef, }
3637 my $item = $builder->build_sample_item(
3639 library => $library->{branchcode},
3640 itype => $itemtype->{itemtype},
3643 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3645 my ( $issuingimpossible, $needsconfirmation );
3648 subtest 'item-level_itypes = 1' => sub {
3651 t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
3652 # Is for loan at item type and item level
3653 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3654 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3655 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3657 # not for loan at item type level
3658 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3659 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3660 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3663 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3664 'Item can not be issued, not for loan at item type level'
3667 # not for loan at item level
3668 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3669 $item->notforloan( 1 )->store;
3670 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3671 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3674 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3675 'Item can not be issued, not for loan at item type level'
3679 subtest 'item-level_itypes = 0' => sub {
3682 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3684 # We set another itemtype for biblioitem
3685 my $itemtype = $builder->build(
3687 source => 'Itemtype',
3688 value => { notforloan => undef, }
3692 # for loan at item type and item level
3693 $item->notforloan(0)->store;
3694 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3695 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3696 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3697 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3699 # not for loan at item type level
3700 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3701 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3702 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3705 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3706 'Item can not be issued, not for loan at item type level'
3709 # not for loan at item level
3710 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3711 $item->notforloan( 1 )->store;
3712 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3713 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3716 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3717 'Item can not be issued, not for loan at item type level'
3721 # TODO test with AllowNotForLoanOverride = 1
3724 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
3727 t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
3728 my $item = $builder->build_sample_item(
3730 onloan => '2018-01-01',
3734 AddReturn( $item->barcode, $item->homebranch );
3735 $item->discard_changes; # refresh
3736 is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
3740 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
3745 t::lib::Mocks::mock_preference('item-level_itypes', 1);
3747 my $issuing_charges = 15;
3748 my $title = 'A title';
3749 my $author = 'Author, An';
3750 my $barcode = 'WHATARETHEODDS';
3752 my $circ = Test::MockModule->new('C4::Circulation');
3754 'GetIssuingCharges',
3756 return $issuing_charges;
3760 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3761 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
3762 my $patron = $builder->build_object({
3763 class => 'Koha::Patrons',
3764 value => { branchcode => $library->id }
3767 my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
3768 my $item_id = Koha::Item->new(
3770 biblionumber => $biblio->biblionumber,
3771 homebranch => $library->id,
3772 holdingbranch => $library->id,
3773 barcode => $barcode,
3774 replacementprice => 23.00,
3775 itype => $itemtype->id
3777 )->store->itemnumber;
3778 my $item = Koha::Items->find( $item_id );
3780 my $context = Test::MockModule->new('C4::Context');
3781 $context->mock( userenv => { branch => $library->id } );
3783 # Check the item out
3784 AddIssue( $patron->unblessed, $item->barcode );
3785 t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
3786 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
3787 my %params_renewal = (
3788 timestamp => { -like => $date . "%" },
3789 module => "CIRCULATION",
3790 action => "RENEWAL",
3792 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
3793 AddRenewal( $patron->id, $item->id, $library->id );
3794 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3795 is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
3797 my $checkouts = $patron->checkouts;
3798 # The following will fail if run on 00:00:00
3799 unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
3801 my $lines = Koha::Account::Lines->search({
3802 borrowernumber => $patron->id,
3803 itemnumber => $item->id
3806 is( $lines->count, 2 );
3808 my $line = $lines->next;
3809 is( $line->debit_type_code, 'RENT', 'The issue of item with issuing charge generates an accountline of the correct type' );
3810 is( $line->branchcode, $library->id, 'AddIssuingCharge correctly sets branchcode' );
3811 is( $line->description, '', 'AddIssue does not set a hardcoded description for the accountline' );
3813 $line = $lines->next;
3814 is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
3815 is( $line->branchcode, $library->id, 'AddRenewal correctly sets branchcode' );
3816 is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
3818 t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
3820 $context = Test::MockModule->new('C4::Context');
3821 $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
3823 my $now = dt_from_string;
3824 $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
3825 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
3826 my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
3827 $sth->execute($item->id, $library->id);
3828 my ($old_stats_size) = $sth->fetchrow_array;
3829 AddRenewal( $patron->id, $item->id, $library->id );
3830 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3831 $sth->execute($item->id, $library->id);
3832 my ($new_stats_size) = $sth->fetchrow_array;
3833 is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
3834 is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
3836 AddReturn( $item->id, $library->id, undef, $date );
3837 AddIssue( $patron->unblessed, $item->barcode, $now );
3838 AddRenewal( $patron->id, $item->id, $library->id, undef, undef, 1 );
3839 my $lines_skipped = Koha::Account::Lines->search({
3840 borrowernumber => $patron->id,
3841 itemnumber => $item->id
3843 is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
3847 subtest 'ProcessOfflinePayment() tests' => sub {
3854 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
3855 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3856 my $result = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
3858 is( $result, 'Success.', 'The right string is returned' );
3860 my $lines = $patron->account->lines;
3861 is( $lines->count, 1, 'line created correctly');
3863 my $line = $lines->next;
3864 is( $line->amount+0, $amount * -1, 'amount picked from params' );
3865 is( $line->branchcode, $library->id, 'branchcode set correctly' );
3869 subtest 'Incremented fee tests' => sub {
3872 my $dt = dt_from_string();
3873 Time::Fake->offset( $dt->epoch );
3875 t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
3878 $builder->build_object( { class => 'Koha::Libraries' } )->store;
3880 $module->mock( 'userenv', sub { { branch => $library->id } } );
3882 my $patron = $builder->build_object(
3884 class => 'Koha::Patrons',
3885 value => { categorycode => $patron_category->{categorycode} }
3889 my $itemtype = $builder->build_object(
3891 class => 'Koha::ItemTypes',
3893 notforloan => undef,
3895 rentalcharge_daily => 1,
3896 rentalcharge_daily_calendar => 0
3901 my $item = $builder->build_sample_item(
3903 library => $library->{branchcode},
3904 itype => $itemtype->id,
3908 is( $itemtype->rentalcharge_daily+0,
3909 1, 'Daily rental charge stored and retreived correctly' );
3910 is( $item->effective_itemtype, $itemtype->id,
3911 "Itemtype set correctly for item" );
3913 my $now = dt_from_string;
3914 my $dt_from = $now->clone;
3915 my $dt_to = $now->clone->add( days => 7 );
3916 my $dt_to_renew = $now->clone->add( days => 13 );
3920 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3921 my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3922 is( $accountline->amount+0, 7,
3923 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
3925 $accountline->delete();
3926 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3927 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3928 is( $accountline->amount+0, 6,
3929 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
3931 $accountline->delete();
3934 t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
3935 $itemtype->rentalcharge_daily_calendar(1)->store();
3937 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3938 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3939 is( $accountline->amount+0, 7,
3940 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
3942 $accountline->delete();
3943 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3944 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3945 is( $accountline->amount+0, 6,
3946 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
3948 $accountline->delete();
3951 my $calendar = C4::Calendar->new( branchcode => $library->id );
3952 # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
3954 ( $dt_from->day_of_week == 6 ) ? 0
3955 : ( $dt_from->day_of_week == 7 ) ? 1
3956 : $dt_from->day_of_week + 1;
3957 my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
3958 $calendar->insert_week_day_holiday(
3959 weekday => $closed_day,
3960 title => 'Test holiday',
3961 description => 'Test holiday'
3964 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3965 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3966 is( $accountline->amount+0, 6,
3967 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
3969 $accountline->delete();
3970 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3971 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3972 is( $accountline->amount+0, 5,
3973 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
3975 $accountline->delete();
3978 $itemtype->rentalcharge(2)->store;
3979 is( $itemtype->rentalcharge+0, 2,
3980 'Rental charge updated and retreived correctly' );
3982 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3984 Koha::Account::Lines->search( { itemnumber => $item->id } );
3985 is( $accountlines->count, '2',
3986 "Fixed charge and accrued charge recorded distinctly" );
3987 $accountlines->delete();
3988 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3989 $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
3990 is( $accountlines->count, '2',
3991 "Fixed charge and accrued charge recorded distinctly, for renewal" );
3992 $accountlines->delete();
3994 $itemtype->rentalcharge(0)->store;
3995 is( $itemtype->rentalcharge+0, 0,
3996 'Rental charge reset and retreived correctly' );
3999 Koha::CirculationRules->set_rule(
4001 categorycode => $patron->categorycode,
4002 itemtype => $itemtype->id,
4003 branchcode => $library->id,
4004 rule_name => 'lengthunit',
4005 rule_value => 'hours',
4009 $itemtype->rentalcharge_hourly('0.25')->store();
4010 is( $itemtype->rentalcharge_hourly,
4011 '0.25', 'Hourly rental charge stored and retreived correctly' );
4013 $dt_to = $now->clone->add( hours => 168 );
4014 $dt_to_renew = $now->clone->add( hours => 312 );
4016 $itemtype->rentalcharge_hourly_calendar(0)->store();
4018 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4019 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4020 is( $accountline->amount + 0, 42,
4021 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)" );
4022 $accountline->delete();
4023 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
4024 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4025 is( $accountline->amount + 0, 36,
4026 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)" );
4027 $accountline->delete();
4030 $itemtype->rentalcharge_hourly_calendar(1)->store();
4032 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4033 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4034 is( $accountline->amount + 0, 36,
4035 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)" );
4036 $accountline->delete();
4037 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
4038 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4039 is( $accountline->amount + 0, 30,
4040 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u" );
4041 $accountline->delete();
4044 $calendar->delete_holiday( weekday => $closed_day );
4046 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4047 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4048 is( $accountline->amount + 0, 42,
4049 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u" );
4050 $accountline->delete();
4051 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
4052 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4053 is( $accountline->amount + 0, 36,
4054 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)" );
4055 $accountline->delete();
4060 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
4063 t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
4064 t::lib::Mocks::mock_preference('item-level_itypes', 1);
4067 $builder->build_object( { class => 'Koha::Libraries' } )->store;
4068 my $patron = $builder->build_object(
4070 class => 'Koha::Patrons',
4071 value => { categorycode => $patron_category->{categorycode} }
4075 my $itemtype = $builder->build_object(
4077 class => 'Koha::ItemTypes',
4081 rentalcharge_daily => 0
4086 my $item = $builder->build_sample_item(
4088 library => $library->id,
4092 itype => $itemtype->id,
4096 my ( $issuingimpossible, $needsconfirmation );
4097 my $dt_from = dt_from_string();
4098 my $dt_due = $dt_from->clone->add( days => 3 );
4100 $itemtype->rentalcharge(1)->store;
4101 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4102 is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
4103 $itemtype->rentalcharge('0')->store;
4104 $itemtype->rentalcharge_daily(1)->store;
4105 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4106 is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
4107 $itemtype->rentalcharge_daily('0')->store;
4110 subtest 'CanBookBeIssued & CircConfirmItemParts' => sub {
4113 t::lib::Mocks::mock_preference('CircConfirmItemParts', 1);
4115 my $patron = $builder->build_object(
4117 class => 'Koha::Patrons',
4118 value => { categorycode => $patron_category->{categorycode} }
4122 my $item = $builder->build_sample_item(
4124 materials => 'includes DVD',
4128 my $dt_due = dt_from_string->add( days => 3 );
4130 my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4131 is_deeply( $needsconfirmation, { ADDITIONAL_MATERIALS => 'includes DVD' }, 'Item needs confirmation of additional parts' );
4134 subtest 'Do not return on renewal (LOST charge)' => sub {
4137 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
4138 my $library = $builder->build_object( { class => "Koha::Libraries" } );
4139 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
4140 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
4142 my $biblio = $builder->build_sample_biblio;
4144 my $item = $builder->build_sample_item(
4146 biblionumber => $biblio->biblionumber,
4147 library => $library->branchcode,
4148 replacementprice => 99.00,
4153 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
4154 AddIssue( $patron->unblessed, $item->barcode );
4156 my $accountline = Koha::Account::Line->new(
4158 borrowernumber => $patron->borrowernumber,
4159 debit_type_code => 'LOST',
4161 itemnumber => $item->itemnumber,
4163 amountoutstanding => 12,
4164 interface => 'something',
4168 # AddRenewal doesn't call _FixAccountForLostAndFound
4169 AddIssue( $patron->unblessed, $item->barcode );
4171 is( $patron->checkouts->count, 1,
4172 'Renewal should not return the item even if a LOST payment has been made earlier'
4176 subtest 'Filling a hold should cancel existing transfer' => sub {
4179 t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
4181 my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
4182 my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
4183 my $patron = $builder->build_object(
4185 class => 'Koha::Patrons',
4187 categorycode => $patron_category->{categorycode},
4188 branchcode => $libraryA->branchcode,
4193 my $item = $builder->build_sample_item({
4194 homebranch => $libraryB->branchcode,
4197 my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4198 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
4200 branchcode => $libraryA->branchcode,
4201 borrowernumber => $patron->borrowernumber,
4202 biblionumber => $item->biblionumber,
4203 itemnumber => $item->itemnumber
4205 my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
4206 is( $reserves->count, 1, "Reserve is placed");
4207 ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4208 my $reserve = $reserves->next;
4209 ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
4210 $reserve->discard_changes;
4211 ok( $reserve->found eq 'W', "Reserve is marked waiting" );
4212 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
4215 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddReturn' => sub {
4219 t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
4220 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4221 my $patron = $builder->build_object(
4223 class => 'Koha::Patrons',
4224 value => { categorycode => $patron_category->{categorycode} }
4228 my $biblionumber = $builder->build_sample_biblio(
4230 branchcode => $library->branchcode,
4234 # And the circulation rule
4235 Koha::CirculationRules->search->delete;
4236 Koha::CirculationRules->set_rules(
4238 categorycode => undef,
4240 branchcode => undef,
4243 lengthunit => 'days',
4249 source => 'CirculationRule',
4251 branchcode => undef,
4252 categorycode => undef,
4254 rule_name => 'lostreturn',
4255 rule_value => 'refund'
4260 subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
4263 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4264 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
4266 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4268 my $item = $builder->build_sample_item(
4270 biblionumber => $biblionumber,
4271 library => $library->branchcode,
4272 replacementprice => '42',
4275 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4276 LostItem( $item->itemnumber, 'cli', 0 );
4277 $item->_result->itemlost(1);
4278 $item->_result->itemlost_on( $lost_on );
4279 $item->_result->update();
4281 my $a = Koha::Account::Lines->search(
4283 itemnumber => $item->id,
4284 borrowernumber => $patron->borrowernumber
4287 ok( $a, "Found accountline for lost fee" );
4288 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4289 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4290 $a = $a->get_from_storage;
4291 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4295 subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
4298 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4299 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4301 my $lost_on = dt_from_string->subtract( days => 6 )->date;
4303 my $item = $builder->build_sample_item(
4305 biblionumber => $biblionumber,
4306 library => $library->branchcode,
4307 replacementprice => '42',
4310 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4311 LostItem( $item->itemnumber, 'cli', 0 );
4312 $item->_result->itemlost(1);
4313 $item->_result->itemlost_on( $lost_on );
4314 $item->_result->update();
4316 my $a = Koha::Account::Lines->search(
4318 itemnumber => $item->id,
4319 borrowernumber => $patron->borrowernumber
4322 ok( $a, "Found accountline for lost fee" );
4323 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4324 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4325 $a = $a->get_from_storage;
4326 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4330 subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
4333 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4334 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4336 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4338 my $item = $builder->build_sample_item(
4340 biblionumber => $biblionumber,
4341 library => $library->branchcode,
4342 replacementprice => '42',
4345 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4346 LostItem( $item->itemnumber, 'cli', 0 );
4347 $item->_result->itemlost(1);
4348 $item->_result->itemlost_on( $lost_on );
4349 $item->_result->update();
4351 my $a = Koha::Account::Lines->search(
4353 itemnumber => $item->id,
4354 borrowernumber => $patron->borrowernumber
4357 ok( $a, "Found accountline for lost fee" );
4358 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4359 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4360 $a = $a->get_from_storage;
4361 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4365 subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
4368 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4369 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4371 my $lost_on = dt_from_string->subtract( days => 8 )->date;
4373 my $item = $builder->build_sample_item(
4375 biblionumber => $biblionumber,
4376 library => $library->branchcode,
4377 replacementprice => '42',
4380 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4381 LostItem( $item->itemnumber, 'cli', 0 );
4382 $item->_result->itemlost(1);
4383 $item->_result->itemlost_on( $lost_on );
4384 $item->_result->update();
4386 my $a = Koha::Account::Lines->search(
4388 itemnumber => $item->id,
4389 borrowernumber => $patron->borrowernumber
4393 ok( $a, "Found accountline for lost fee" );
4394 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4395 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4396 $a = $a->get_from_storage;
4397 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4402 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddIssue' => sub {
4406 t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
4407 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4408 my $patron = $builder->build_object(
4410 class => 'Koha::Patrons',
4411 value => { categorycode => $patron_category->{categorycode} }
4414 my $patron2 = $builder->build_object(
4416 class => 'Koha::Patrons',
4417 value => { categorycode => $patron_category->{categorycode} }
4421 my $biblionumber = $builder->build_sample_biblio(
4423 branchcode => $library->branchcode,
4427 # And the circulation rule
4428 Koha::CirculationRules->search->delete;
4429 Koha::CirculationRules->set_rules(
4431 categorycode => undef,
4433 branchcode => undef,
4436 lengthunit => 'days',
4442 source => 'CirculationRule',
4444 branchcode => undef,
4445 categorycode => undef,
4447 rule_name => 'lostreturn',
4448 rule_value => 'refund'
4453 subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
4456 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4457 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
4459 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4461 my $item = $builder->build_sample_item(
4463 biblionumber => $biblionumber,
4464 library => $library->branchcode,
4465 replacementprice => '42',
4468 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4469 LostItem( $item->itemnumber, 'cli', 0 );
4470 $item->_result->itemlost(1);
4471 $item->_result->itemlost_on( $lost_on );
4472 $item->_result->update();
4474 my $a = Koha::Account::Lines->search(
4476 itemnumber => $item->id,
4477 borrowernumber => $patron->borrowernumber
4480 ok( $a, "Found accountline for lost fee" );
4481 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4482 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4483 $a = $a->get_from_storage;
4484 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4489 subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
4492 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4493 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4495 my $lost_on = dt_from_string->subtract( days => 6 )->date;
4497 my $item = $builder->build_sample_item(
4499 biblionumber => $biblionumber,
4500 library => $library->branchcode,
4501 replacementprice => '42',
4504 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4505 LostItem( $item->itemnumber, 'cli', 0 );
4506 $item->_result->itemlost(1);
4507 $item->_result->itemlost_on( $lost_on );
4508 $item->_result->update();
4510 my $a = Koha::Account::Lines->search(
4512 itemnumber => $item->id,
4513 borrowernumber => $patron->borrowernumber
4516 ok( $a, "Found accountline for lost fee" );
4517 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4518 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4519 $a = $a->get_from_storage;
4520 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4524 subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
4527 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4528 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4530 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4532 my $item = $builder->build_sample_item(
4534 biblionumber => $biblionumber,
4535 library => $library->branchcode,
4536 replacementprice => '42',
4539 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4540 LostItem( $item->itemnumber, 'cli', 0 );
4541 $item->_result->itemlost(1);
4542 $item->_result->itemlost_on( $lost_on );
4543 $item->_result->update();
4545 my $a = Koha::Account::Lines->search(
4547 itemnumber => $item->id,
4548 borrowernumber => $patron->borrowernumber
4551 ok( $a, "Found accountline for lost fee" );
4552 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4553 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4554 $a = $a->get_from_storage;
4555 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4559 subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
4562 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4563 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4565 my $lost_on = dt_from_string->subtract( days => 8 )->date;
4567 my $item = $builder->build_sample_item(
4569 biblionumber => $biblionumber,
4570 library => $library->branchcode,
4571 replacementprice => '42',
4574 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4575 LostItem( $item->itemnumber, 'cli', 0 );
4576 $item->_result->itemlost(1);
4577 $item->_result->itemlost_on( $lost_on );
4578 $item->_result->update();
4580 my $a = Koha::Account::Lines->search(
4582 itemnumber => $item->id,
4583 borrowernumber => $patron->borrowernumber
4587 ok( $a, "Found accountline for lost fee" );
4588 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4589 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4590 $a = $a->get_from_storage;
4591 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4596 subtest 'transferbook tests' => sub {
4600 { C4::Circulation::transferbook({}); }
4601 'Koha::Exceptions::MissingParameter',
4602 'Koha::Patron->store raises an exception on missing params';
4605 { C4::Circulation::transferbook({to_branch=>'anything'}); }
4606 'Koha::Exceptions::MissingParameter',
4607 'Koha::Patron->store raises an exception on missing params';
4610 { C4::Circulation::transferbook({from_branch=>'anything'}); }
4611 'Koha::Exceptions::MissingParameter',
4612 'Koha::Patron->store raises an exception on missing params';
4614 my ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here'});
4615 is( $doreturn, 0, "No return without barcode");
4616 ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
4617 is( $messages->{BadBarcode}, undef, "No barcode passed means undef BadBarcode" );
4619 ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here',barcode=>'BadBarcode'});
4620 is( $doreturn, 0, "No return without barcode");
4621 ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
4622 is( $messages->{BadBarcode}, 'BadBarcode', "No barcode passed means undef BadBarcode" );
4626 subtest 'Checkout should correctly terminate a transfer' => sub {
4629 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
4630 my $patron_1 = $builder->build_object(
4632 class => 'Koha::Patrons',
4633 value => { branchcode => $library_1->branchcode }
4636 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
4637 my $patron_2 = $builder->build_object(
4639 class => 'Koha::Patrons',
4640 value => { branchcode => $library_2->branchcode }
4644 my $item = $builder->build_sample_item(
4646 library => $library_1->branchcode,
4650 t::lib::Mocks::mock_userenv( { branchcode => $library_1->branchcode } );
4651 my $reserve_id = AddReserve(
4653 branchcode => $library_2->branchcode,
4654 borrowernumber => $patron_2->borrowernumber,
4655 biblionumber => $item->biblionumber,
4656 itemnumber => $item->itemnumber,
4661 my $do_transfer = 1;
4662 ModItemTransfer( $item->itemnumber, $library_1->branchcode,
4663 $library_2->branchcode );
4664 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4665 GetOtherReserves( $item->itemnumber )
4666 ; # To put the Reason, it's what does returns.pl...
4667 my $hold = Koha::Holds->find($reserve_id);
4668 is( $hold->found, 'T', 'Hold is in transit' );
4669 my $transfer = $item->get_transfer;
4670 is( $transfer->frombranch, $library_1->branchcode );
4671 is( $transfer->tobranch, $library_2->branchcode );
4672 is( $transfer->reason, 'Reserve' );
4674 t::lib::Mocks::mock_userenv( { branchcode => $library_2->branchcode } );
4675 AddIssue( $patron_1->unblessed, $item->barcode );
4676 $transfer = $transfer->get_from_storage;
4677 isnt( $transfer->datearrived, undef );
4678 $hold = $hold->get_from_storage;
4679 is( $hold->found, undef, 'Hold is waiting' );
4680 is( $hold->priority, 1, );
4683 subtest 'AddIssue records staff who checked out item if appropriate' => sub {
4686 $module->mock( 'userenv', sub { { branch => $library->{id} } } );
4688 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4689 my $patron = $builder->build_object(
4691 class => 'Koha::Patrons',
4692 value => { categorycode => $patron_category->{categorycode} }
4695 my $issuer = $builder->build_object(
4697 class => 'Koha::Patrons',
4698 value => { categorycode => $patron_category->{categorycode} }
4701 my $item = $builder->build_sample_item(
4703 library => $library->{branchcode}
4707 $module->mock( 'userenv', sub { { branch => $library->id, number => $issuer->{borrowernumber} } } );
4709 my $dt_from = dt_from_string();
4710 my $dt_to = dt_from_string()->add( days => 7 );
4712 my $issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4714 is( $issue->issuer, undef, "Staff who checked out the item not recorded when RecordStaffUserOnCheckout turned off" );
4716 t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 1);
4719 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4721 is( $issue->issuer, $issuer->{borrowernumber}, "Staff who checked out the item recorded when RecordStaffUserOnCheckout turned on" );
4724 $schema->storage->txn_rollback;
4725 C4::Context->clear_syspref_cache();
4726 $branches = Koha::Libraries->search();
4727 for my $branch ( $branches->next ) {
4728 my $key = $branch->branchcode . "_holidays";
4729 $cache->clear_from_cache($key);