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 => 47;
23 use Test::Deep qw( cmp_deeply );
28 use POSIX qw( floor );
30 use t::lib::TestBuilder;
39 use C4::Overdues qw(UpdateFine CalcFine);
43 use Koha::Item::Transfers;
47 use Koha::CirculationRules;
48 use Koha::Subscriptions;
49 use Koha::Account::Lines;
50 use Koha::Account::Offsets;
55 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
59 my ( $error, $question, $alert ) = @_;
61 $s = %$error ? ' (error: ' . join( ' ', keys %$error ) . ')' : '';
62 $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
63 $s .= %$alert ? ' (alert: ' . join( ' ', keys %$alert ) . ')' : '';
67 sub test_debarment_on_checkout {
69 my $item = $params->{item};
70 my $library = $params->{library};
71 my $patron = $params->{patron};
72 my $due_date = $params->{due_date} || dt_from_string;
73 my $return_date = $params->{return_date} || dt_from_string;
74 my $expected_expiration_date = $params->{expiration_date};
76 $expected_expiration_date = output_pref(
78 dt => $expected_expiration_date,
84 my $line_number = $caller[2];
85 AddIssue( $patron, $item->{barcode}, $due_date );
87 my ( undef, $message ) = AddReturn( $item->{barcode}, $library->{branchcode}, undef, $return_date );
88 is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
89 or diag('AddReturn returned message ' . Dumper $message );
90 my $debarments = Koha::Patron::Debarments::GetDebarments(
91 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
92 is( scalar(@$debarments), 1, 'Test at line ' . $line_number );
94 is( $debarments->[0]->{expiration},
95 $expected_expiration_date, 'Test at line ' . $line_number );
96 Koha::Patron::Debarments::DelUniqueDebarment(
97 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
100 my $schema = Koha::Database->schema;
101 $schema->storage->txn_begin;
102 my $builder = t::lib::TestBuilder->new;
103 my $dbh = C4::Context->dbh;
105 # Prevent random failures by mocking ->now
106 my $now_value = dt_from_string;
107 my $mocked_datetime = Test::MockModule->new('DateTime');
108 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
110 my $cache = Koha::Caches->get_instance();
111 $dbh->do(q|DELETE FROM special_holidays|);
112 $dbh->do(q|DELETE FROM repeatable_holidays|);
113 my $branches = Koha::Libraries->search();
114 for my $branch ( $branches->next ) {
115 my $key = $branch->branchcode . "_holidays";
116 $cache->clear_from_cache($key);
119 # Start with a clean slate
120 $dbh->do('DELETE FROM issues');
121 $dbh->do('DELETE FROM borrowers');
123 my $library = $builder->build({
126 my $library2 = $builder->build({
129 my $itemtype = $builder->build(
131 source => 'Itemtype',
135 rentalcharge_daily => 0,
136 defaultreplacecost => undef,
141 my $patron_category = $builder->build(
143 source => 'Category',
145 category_type => 'P',
147 BlockExpiredPatronOpacActions => -1, # Pick the pref value
152 my $CircControl = C4::Context->preference('CircControl');
153 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
156 homebranch => $library2->{branchcode},
157 holdingbranch => $library2->{branchcode}
161 branchcode => $library2->{branchcode}
164 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
166 # No userenv, PickupLibrary
167 t::lib::Mocks::mock_preference('IndependentBranches', '0');
168 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
170 C4::Context->preference('CircControl'),
172 'CircControl changed to PickupLibrary'
175 C4::Circulation::_GetCircControlBranch($item, $borrower),
176 $item->{$HomeOrHoldingBranch},
177 '_GetCircControlBranch returned item branch (no userenv defined)'
180 # No userenv, PatronLibrary
181 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
183 C4::Context->preference('CircControl'),
185 'CircControl changed to PatronLibrary'
188 C4::Circulation::_GetCircControlBranch($item, $borrower),
189 $borrower->{branchcode},
190 '_GetCircControlBranch returned borrower branch'
193 # No userenv, ItemHomeLibrary
194 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
196 C4::Context->preference('CircControl'),
198 'CircControl changed to ItemHomeLibrary'
201 $item->{$HomeOrHoldingBranch},
202 C4::Circulation::_GetCircControlBranch($item, $borrower),
203 '_GetCircControlBranch returned item branch'
207 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
208 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
210 # Userenv set, PickupLibrary
211 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
213 C4::Context->preference('CircControl'),
215 'CircControl changed to PickupLibrary'
218 C4::Circulation::_GetCircControlBranch($item, $borrower),
219 $library2->{branchcode},
220 '_GetCircControlBranch returned current branch'
223 # Userenv set, PatronLibrary
224 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
226 C4::Context->preference('CircControl'),
228 'CircControl changed to PatronLibrary'
231 C4::Circulation::_GetCircControlBranch($item, $borrower),
232 $borrower->{branchcode},
233 '_GetCircControlBranch returned borrower branch'
236 # Userenv set, ItemHomeLibrary
237 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
239 C4::Context->preference('CircControl'),
241 'CircControl changed to ItemHomeLibrary'
244 C4::Circulation::_GetCircControlBranch($item, $borrower),
245 $item->{$HomeOrHoldingBranch},
246 '_GetCircControlBranch returned item branch'
249 # Reset initial configuration
250 t::lib::Mocks::mock_preference('CircControl', $CircControl);
252 C4::Context->preference('CircControl'),
254 'CircControl reset to its initial value'
257 # Set a simple circ policy
258 $dbh->do('DELETE FROM circulation_rules');
259 Koha::CirculationRules->set_rules(
261 categorycode => undef,
265 reservesallowed => 25,
267 lengthunit => 'days',
268 renewalsallowed => 1,
270 norenewalbefore => undef,
278 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
279 subtest "CanBookBeRenewed tests" => sub {
282 C4::Context->set_preference('ItemsDeniedRenewal','');
283 # Generate test biblio
284 my $biblio = $builder->build_sample_biblio();
286 my $branch = $library2->{branchcode};
288 my $item_1 = $builder->build_sample_item(
290 biblionumber => $biblio->biblionumber,
292 replacementprice => 12.00,
296 $reused_itemnumber_1 = $item_1->itemnumber;
298 my $item_2 = $builder->build_sample_item(
300 biblionumber => $biblio->biblionumber,
302 replacementprice => 23.00,
306 $reused_itemnumber_2 = $item_2->itemnumber;
308 my $item_3 = $builder->build_sample_item(
310 biblionumber => $biblio->biblionumber,
312 replacementprice => 23.00,
318 my %renewing_borrower_data = (
320 surname => 'Renewal',
321 categorycode => $patron_category->{categorycode},
322 branchcode => $branch,
325 my %reserving_borrower_data = (
326 firstname => 'Katrin',
327 surname => 'Reservation',
328 categorycode => $patron_category->{categorycode},
329 branchcode => $branch,
332 my %hold_waiting_borrower_data = (
334 surname => 'Reservation',
335 categorycode => $patron_category->{categorycode},
336 branchcode => $branch,
339 my %restricted_borrower_data = (
340 firstname => 'Alice',
341 surname => 'Reservation',
342 categorycode => $patron_category->{categorycode},
343 debarred => '3228-01-01',
344 branchcode => $branch,
347 my %expired_borrower_data = (
350 categorycode => $patron_category->{categorycode},
351 branchcode => $branch,
352 dateexpiry => dt_from_string->subtract( months => 1 ),
355 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
356 my $reserving_borrowernumber = Koha::Patron->new(\%reserving_borrower_data)->store->borrowernumber;
357 my $hold_waiting_borrowernumber = Koha::Patron->new(\%hold_waiting_borrower_data)->store->borrowernumber;
358 my $restricted_borrowernumber = Koha::Patron->new(\%restricted_borrower_data)->store->borrowernumber;
359 my $expired_borrowernumber = Koha::Patron->new(\%expired_borrower_data)->store->borrowernumber;
361 my $renewing_borrower_obj = Koha::Patrons->find( $renewing_borrowernumber );
362 my $renewing_borrower = $renewing_borrower_obj->unblessed;
363 my $restricted_borrower = Koha::Patrons->find( $restricted_borrowernumber )->unblessed;
364 my $expired_borrower = Koha::Patrons->find( $expired_borrowernumber )->unblessed;
371 my $checkitem = undef;
374 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
375 my $datedue = dt_from_string( $issue->date_due() );
376 is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
378 my $issue2 = AddIssue( $renewing_borrower, $item_2->barcode);
379 $datedue = dt_from_string( $issue->date_due() );
380 is (defined $issue2, 1, "Item 2 checked out, due date: " . $issue2->date_due());
383 my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
384 is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}");
386 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
387 is( $renewokay, 1, 'Can renew, no holds for this title or item');
390 # Biblio-level hold, renewal test
393 branchcode => $branch,
394 borrowernumber => $reserving_borrowernumber,
395 biblionumber => $biblio->biblionumber,
396 priority => $priority,
397 reservation_date => $resdate,
398 expiration_date => $expdate,
400 itemnumber => $checkitem,
405 # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
406 Koha::CirculationRules->set_rule(
408 categorycode => undef,
411 rule_name => 'onshelfholds',
415 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
416 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
417 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
418 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
419 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
421 # Now let's add an item level hold, we should no longer be able to renew the item
422 my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
424 borrowernumber => $hold_waiting_borrowernumber,
425 biblionumber => $biblio->biblionumber,
426 itemnumber => $item_1->itemnumber,
427 branchcode => $branch,
431 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
432 is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
435 # 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
436 # be able to renew these items
437 $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
439 borrowernumber => $hold_waiting_borrowernumber,
440 biblionumber => $biblio->biblionumber,
441 itemnumber => $item_3->itemnumber,
442 branchcode => $branch,
447 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
448 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
449 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
450 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
451 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
453 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
454 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
455 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
457 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
458 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
459 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
461 my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
462 my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber )->unblessed;
463 AddIssue($reserving_borrower, $item_3->barcode);
464 my $reserve = $dbh->selectrow_hashref(
465 'SELECT * FROM old_reserves WHERE reserve_id = ?',
469 is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
471 # Item-level hold, renewal test
474 branchcode => $branch,
475 borrowernumber => $reserving_borrowernumber,
476 biblionumber => $biblio->biblionumber,
477 priority => $priority,
478 reservation_date => $resdate,
479 expiration_date => $expdate,
481 itemnumber => $item_1->itemnumber,
486 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
487 is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
488 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
490 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber, 1);
491 is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
493 # Items can't fill hold for reasons
494 $item_1->notforloan(1)->store;
495 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
496 is( $renewokay, 1, 'Can renew, item is marked not for loan, hold does not block');
497 $item_1->set({notforloan => 0, itype => $itemtype })->store;
499 # FIXME: Add more for itemtype not for loan etc.
501 # Restricted users cannot renew when RestrictionBlockRenewing is enabled
502 my $item_5 = $builder->build_sample_item(
504 biblionumber => $biblio->biblionumber,
506 replacementprice => 23.00,
510 my $datedue5 = AddIssue($restricted_borrower, $item_5->barcode);
511 is (defined $datedue5, 1, "Item with date due checked out, due date: $datedue5");
513 t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
514 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
515 is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
516 ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $item_5->itemnumber);
517 is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
519 # Users cannot renew an overdue item
520 my $item_6 = $builder->build_sample_item(
522 biblionumber => $biblio->biblionumber,
524 replacementprice => 23.00,
529 my $item_7 = $builder->build_sample_item(
531 biblionumber => $biblio->biblionumber,
533 replacementprice => 23.00,
538 my $datedue6 = AddIssue( $renewing_borrower, $item_6->barcode);
539 is (defined $datedue6, 1, "Item 2 checked out, due date: ".$datedue6->date_due);
541 my $now = dt_from_string();
542 my $five_weeks = DateTime::Duration->new(weeks => 5);
543 my $five_weeks_ago = $now - $five_weeks;
544 t::lib::Mocks::mock_preference('finesMode', 'production');
546 my $passeddatedue1 = AddIssue($renewing_borrower, $item_7->barcode, $five_weeks_ago);
547 is (defined $passeddatedue1, 1, "Item with passed date due checked out, due date: " . $passeddatedue1->date_due);
549 my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower->{categorycode}, $branch, $five_weeks_ago, $now );
550 C4::Overdues::UpdateFine(
552 issue_id => $passeddatedue1->id(),
553 itemnumber => $item_7->itemnumber,
554 borrowernumber => $renewing_borrower->{borrowernumber},
556 due => Koha::DateUtils::output_pref($five_weeks_ago)
560 t::lib::Mocks::mock_preference('RenewalLog', 0);
561 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
562 my %params_renewal = (
563 timestamp => { -like => $date . "%" },
564 module => "CIRCULATION",
568 timestamp => { -like => $date . "%" },
569 module => "CIRCULATION",
572 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );
573 my $dt = dt_from_string();
574 Time::Fake->offset( $dt->epoch );
575 my $datedue1 = AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
576 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
577 is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
578 isnt (DateTime->compare($datedue1, $dt), 0, "AddRenewal returned a good duedate");
581 t::lib::Mocks::mock_preference('RenewalLog', 1);
582 $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
583 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
584 AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
585 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
586 is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
588 my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
589 is( $fines->count, 2, 'AddRenewal left both fines' );
590 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
591 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
595 my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
596 my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
597 AddIssue( $renewing_borrower,$item_7->barcode,Koha::DateUtils::output_pref({str=>$datedue6->date_due, dateformat =>'iso'}),0,$date, 0, undef );
598 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
599 is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
600 $new_log_size = Koha::ActionLogs->count( \%params_issue );
601 is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
603 $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
606 t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
607 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
608 is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
609 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
610 is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
613 $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
617 # Test automatic renewal before value for "norenewalbefore" in policy is set
618 # In this case automatic renewal is not permitted prior to due date
619 my $item_4 = $builder->build_sample_item(
621 biblionumber => $biblio->biblionumber,
623 replacementprice => 16.00,
628 $issue = AddIssue( $renewing_borrower, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
629 ( $renewokay, $error ) =
630 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
631 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
632 is( $error, 'auto_too_soon',
633 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
636 branchcode => $branch,
637 borrowernumber => $reserving_borrowernumber,
638 biblionumber => $biblio->biblionumber,
639 itemnumber => $bibitems,
640 priority => $priority,
641 reservation_date => $resdate,
642 expiration_date => $expdate,
645 itemnumber => $item_4->itemnumber,
649 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
650 is( $renewokay, 0, 'Still should not be able to renew' );
651 is( $error, 'on_reserve', 'returned code is on_reserve, reserve checked when not checking for cron' );
652 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, undef, 1 );
653 is( $renewokay, 0, 'Still should not be able to renew' );
654 is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked when checking for cron' );
655 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
656 is( $renewokay, 0, 'Still should not be able to renew' );
657 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
658 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1, 1 );
659 is( $renewokay, 0, 'Still should not be able to renew' );
660 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
661 $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
662 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
663 is( $renewokay, 0, 'Still should not be able to renew' );
664 is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
665 ModReserveCancelAll($item_4->itemnumber, $reserving_borrowernumber);
669 $renewing_borrower_obj->autorenew_checkouts(0)->store;
670 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
671 is( $renewokay, 1, 'No renewal before is undef, but patron opted out of auto_renewal' );
672 $renewing_borrower_obj->autorenew_checkouts(1)->store;
676 # Test premature manual renewal
677 Koha::CirculationRules->set_rule(
679 categorycode => undef,
682 rule_name => 'norenewalbefore',
687 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
688 is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
689 is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
692 # Test 'exact time' setting for syspref NoRenewalBeforePrecision
693 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
695 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
696 $datedue->clone->add( days => -7 ),
697 'Bug 14395: Renewals permitted 7 days before due date, as expected'
701 # Test 'date' setting for syspref NoRenewalBeforePrecision
702 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
704 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
705 $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
706 'Bug 14395: Renewals permitted 7 days before due date, as expected'
710 # Test premature automatic renewal
711 ( $renewokay, $error ) =
712 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
713 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
714 is( $error, 'auto_too_soon',
715 'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
718 $renewing_borrower_obj->autorenew_checkouts(0)->store;
719 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
720 is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
721 is( $error, 'too_soon', 'Error is too_soon, no auto' );
722 $renewing_borrower_obj->autorenew_checkouts(1)->store;
724 # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
725 # and test automatic renewal again
726 $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
727 ( $renewokay, $error ) =
728 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
729 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
730 is( $error, 'auto_too_soon',
731 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
734 $renewing_borrower_obj->autorenew_checkouts(0)->store;
735 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
736 is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
737 is( $error, 'too_soon', 'Error is too_soon, no auto' );
738 $renewing_borrower_obj->autorenew_checkouts(1)->store;
740 # Change policy so that loans can be renewed 99 days prior to the due date
741 # and test automatic renewal again
742 $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
743 ( $renewokay, $error ) =
744 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
745 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
746 is( $error, 'auto_renew',
747 'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
750 $renewing_borrower_obj->autorenew_checkouts(0)->store;
751 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
752 is( $renewokay, 1, 'No renewal before is 99, patron opted out of auto_renewal so can renew' );
753 $renewing_borrower_obj->autorenew_checkouts(1)->store;
755 subtest "too_late_renewal / no_auto_renewal_after" => sub {
757 my $item_to_auto_renew = $builder->build(
760 biblionumber => $biblio->biblionumber,
761 homebranch => $branch,
762 holdingbranch => $branch,
767 my $ten_days_before = dt_from_string->add( days => -10 );
768 my $ten_days_ahead = dt_from_string->add( days => 10 );
769 AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
771 Koha::CirculationRules->set_rules(
773 categorycode => undef,
777 norenewalbefore => '7',
778 no_auto_renewal_after => '9',
782 ( $renewokay, $error ) =
783 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
784 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
785 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
787 Koha::CirculationRules->set_rules(
789 categorycode => undef,
793 norenewalbefore => '7',
794 no_auto_renewal_after => '10',
798 ( $renewokay, $error ) =
799 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
800 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
801 is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
803 Koha::CirculationRules->set_rules(
805 categorycode => undef,
809 norenewalbefore => '7',
810 no_auto_renewal_after => '11',
814 ( $renewokay, $error ) =
815 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
816 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
817 is( $error, 'auto_too_soon', 'Cannot auto renew, too soon - no_auto_renewal_after is defined(returned code is auto_too_soon)' );
819 Koha::CirculationRules->set_rules(
821 categorycode => undef,
825 norenewalbefore => '10',
826 no_auto_renewal_after => '11',
830 ( $renewokay, $error ) =
831 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
832 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
833 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
835 Koha::CirculationRules->set_rules(
837 categorycode => undef,
841 norenewalbefore => '10',
842 no_auto_renewal_after => undef,
843 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
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 renew, too late(returned code is auto_too_late)' );
852 Koha::CirculationRules->set_rules(
854 categorycode => undef,
858 norenewalbefore => '7',
859 no_auto_renewal_after => '15',
860 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
864 ( $renewokay, $error ) =
865 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
866 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
867 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
869 Koha::CirculationRules->set_rules(
871 categorycode => undef,
875 norenewalbefore => '10',
876 no_auto_renewal_after => undef,
877 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 1 ),
881 ( $renewokay, $error ) =
882 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
883 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
884 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
887 subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew & OPACFineNoRenewalsIncludeCredit" => sub {
889 my $item_to_auto_renew = $builder->build({
892 biblionumber => $biblio->biblionumber,
893 homebranch => $branch,
894 holdingbranch => $branch,
898 my $ten_days_before = dt_from_string->add( days => -10 );
899 my $ten_days_ahead = dt_from_string->add( days => 10 );
900 AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
902 Koha::CirculationRules->set_rules(
904 categorycode => undef,
908 norenewalbefore => '10',
909 no_auto_renewal_after => '11',
913 C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
914 C4::Context->set_preference('OPACFineNoRenewals','10');
915 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
916 my $fines_amount = 5;
917 my $account = Koha::Account->new({patron_id => $renewing_borrowernumber});
920 amount => $fines_amount,
923 item_id => $item_to_auto_renew->{itemnumber},
924 description => "Some fines"
926 )->status('RETURNED')->store;
927 ( $renewokay, $error ) =
928 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
929 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
930 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
934 amount => $fines_amount,
937 item_id => $item_to_auto_renew->{itemnumber},
938 description => "Some fines"
940 )->status('RETURNED')->store;
941 ( $renewokay, $error ) =
942 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
943 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
944 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
948 amount => $fines_amount,
951 item_id => $item_to_auto_renew->{itemnumber},
952 description => "Some fines"
954 )->status('RETURNED')->store;
955 ( $renewokay, $error ) =
956 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
957 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
958 is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
960 $account->add_credit(
962 amount => $fines_amount,
965 description => "Some payment"
968 ( $renewokay, $error ) =
969 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
970 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
971 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
973 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','0');
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_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
979 $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
980 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
983 subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
985 my $item_to_auto_renew = $builder->build({
988 biblionumber => $biblio->biblionumber,
989 homebranch => $branch,
990 holdingbranch => $branch,
994 Koha::CirculationRules->set_rules(
996 categorycode => undef,
1000 norenewalbefore => 10,
1001 no_auto_renewal_after => 11,
1006 my $ten_days_before = dt_from_string->add( days => -10 );
1007 my $ten_days_ahead = dt_from_string->add( days => 10 );
1009 # Patron is expired and BlockExpiredPatronOpacActions=0
1010 # => auto renew is allowed
1011 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
1012 my $patron = $expired_borrower;
1013 my $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1014 ( $renewokay, $error ) =
1015 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
1016 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1017 is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
1018 Koha::Checkouts->find( $checkout->issue_id )->delete;
1021 # Patron is expired and BlockExpiredPatronOpacActions=1
1022 # => auto renew is not allowed
1023 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1024 $patron = $expired_borrower;
1025 $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1026 ( $renewokay, $error ) =
1027 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
1028 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1029 is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
1030 Koha::Checkouts->find( $checkout->issue_id )->delete;
1033 # Patron is not expired and BlockExpiredPatronOpacActions=1
1034 # => auto renew is allowed
1035 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1036 $patron = $renewing_borrower;
1037 $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1038 ( $renewokay, $error ) =
1039 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
1040 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1041 is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
1042 Koha::Checkouts->find( $checkout->issue_id )->delete;
1045 subtest "GetLatestAutoRenewDate" => sub {
1047 my $item_to_auto_renew = $builder->build(
1050 biblionumber => $biblio->biblionumber,
1051 homebranch => $branch,
1052 holdingbranch => $branch,
1057 my $ten_days_before = dt_from_string->add( days => -10 );
1058 my $ten_days_ahead = dt_from_string->add( days => 10 );
1059 AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1060 Koha::CirculationRules->set_rules(
1062 categorycode => undef,
1063 branchcode => undef,
1066 norenewalbefore => '7',
1067 no_auto_renewal_after => '',
1068 no_auto_renewal_after_hard_limit => undef,
1072 my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1073 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' );
1074 my $five_days_before = dt_from_string->add( days => -5 );
1075 Koha::CirculationRules->set_rules(
1077 categorycode => undef,
1078 branchcode => undef,
1081 norenewalbefore => '10',
1082 no_auto_renewal_after => '5',
1083 no_auto_renewal_after_hard_limit => undef,
1087 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1088 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1089 $five_days_before->truncate( to => 'minute' ),
1090 'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
1092 my $five_days_ahead = dt_from_string->add( days => 5 );
1093 $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
1094 $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
1095 $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
1096 Koha::CirculationRules->set_rules(
1098 categorycode => undef,
1099 branchcode => undef,
1102 norenewalbefore => '10',
1103 no_auto_renewal_after => '15',
1104 no_auto_renewal_after_hard_limit => undef,
1108 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1109 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1110 $five_days_ahead->truncate( to => 'minute' ),
1111 'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
1113 my $two_days_ahead = dt_from_string->add( days => 2 );
1114 Koha::CirculationRules->set_rules(
1116 categorycode => undef,
1117 branchcode => undef,
1120 norenewalbefore => '10',
1121 no_auto_renewal_after => '',
1122 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1126 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1127 is( $latest_auto_renew_date->truncate( to => 'day' ),
1128 $two_days_ahead->truncate( to => 'day' ),
1129 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
1131 Koha::CirculationRules->set_rules(
1133 categorycode => undef,
1134 branchcode => undef,
1137 norenewalbefore => '10',
1138 no_auto_renewal_after => '15',
1139 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1143 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1144 is( $latest_auto_renew_date->truncate( to => 'day' ),
1145 $two_days_ahead->truncate( to => 'day' ),
1146 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
1152 # set policy to forbid renewals
1153 Koha::CirculationRules->set_rules(
1155 categorycode => undef,
1156 branchcode => undef,
1159 norenewalbefore => undef,
1160 renewalsallowed => 0,
1165 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
1166 is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
1167 is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
1169 # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
1170 t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
1171 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1173 C4::Overdues::UpdateFine(
1175 issue_id => $issue->id(),
1176 itemnumber => $item_1->itemnumber,
1177 borrowernumber => $renewing_borrower->{borrowernumber},
1180 due => Koha::DateUtils::output_pref($datedue)
1184 my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower->{borrowernumber} })->next();
1185 is( $line->debit_type_code, 'OVERDUE', 'Account line type is OVERDUE' );
1186 is( $line->status, 'UNRETURNED', 'Account line status is UNRETURNED' );
1187 is( $line->amountoutstanding+0, 15, 'Account line amount outstanding is 15.00' );
1188 is( $line->amount+0, 15, 'Account line amount is 15.00' );
1189 is( $line->issue_id, $issue->id, 'Account line issue id matches' );
1191 my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
1192 is( $offset->type, 'OVERDUE', 'Account offset type is Fine' );
1193 is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
1195 t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
1196 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
1198 LostItem( $item_1->itemnumber, 'test', 1 );
1200 $line = Koha::Account::Lines->find($line->id);
1201 is( $line->debit_type_code, 'OVERDUE', 'Account type remains as OVERDUE' );
1202 isnt( $line->status, 'UNRETURNED', 'Account status correctly changed from UNRETURNED to RETURNED' );
1204 my $item = Koha::Items->find($item_1->itemnumber);
1205 ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
1206 my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
1207 is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
1209 my $total_due = $dbh->selectrow_array(
1210 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1211 undef, $renewing_borrower->{borrowernumber}
1214 is( $total_due+0, 15, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
1216 C4::Context->dbh->do("DELETE FROM accountlines");
1218 C4::Overdues::UpdateFine(
1220 issue_id => $issue2->id(),
1221 itemnumber => $item_2->itemnumber,
1222 borrowernumber => $renewing_borrower->{borrowernumber},
1225 due => Koha::DateUtils::output_pref($datedue)
1229 LostItem( $item_2->itemnumber, 'test', 0 );
1231 my $item2 = Koha::Items->find($item_2->itemnumber);
1232 ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
1233 ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
1235 $total_due = $dbh->selectrow_array(
1236 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1237 undef, $renewing_borrower->{borrowernumber}
1240 ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
1242 my $future = dt_from_string();
1243 $future->add( days => 7 );
1244 my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
1245 ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
1247 # Users cannot renew any item if there is an overdue item
1248 t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
1249 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
1250 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1251 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
1252 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1254 my $manager = $builder->build_object({ class => "Koha::Patrons" });
1255 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
1256 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1257 $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
1258 LostItem( $item_3->itemnumber, 'test', 0 );
1259 my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
1260 is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
1262 $accountline->description,
1263 sprintf( "%s %s %s",
1264 $item_3->biblio->title || '',
1265 $item_3->barcode || '',
1266 $item_3->itemcallnumber || '' ),
1267 "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1271 subtest "GetUpcomingDueIssues" => sub {
1274 my $branch = $library2->{branchcode};
1276 #Create another record
1277 my $biblio2 = $builder->build_sample_biblio();
1280 my $item_1 = Koha::Items->find($reused_itemnumber_1);
1281 my $item_2 = Koha::Items->find($reused_itemnumber_2);
1282 my $item_3 = $builder->build_sample_item(
1284 biblionumber => $biblio2->biblionumber,
1292 my %a_borrower_data = (
1293 firstname => 'Fridolyn',
1294 surname => 'SOMERS',
1295 categorycode => $patron_category->{categorycode},
1296 branchcode => $branch,
1299 my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1300 my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber )->unblessed;
1302 my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1303 my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1304 my $today = DateTime->today(time_zone => C4::Context->tz());
1306 my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1307 my $datedue = dt_from_string( $issue->date_due() );
1308 my $issue2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1309 my $datedue2 = dt_from_string( $issue->date_due() );
1313 # GetUpcomingDueIssues tests
1315 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1316 is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1319 #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1320 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1321 is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1324 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1325 is ( scalar( @$upcoming_dues ), 1,
1326 "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1329 # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1331 my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1333 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1334 is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1336 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1337 is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1339 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1340 is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1342 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1343 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1345 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1346 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1348 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1349 is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1353 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1354 my $branch = $library2->{branchcode};
1356 my $biblio = $builder->build_sample_biblio();
1359 my $item = $builder->build_sample_item(
1361 biblionumber => $biblio->biblionumber,
1368 my %a_borrower_data = (
1369 firstname => 'Kyle',
1371 categorycode => $patron_category->{categorycode},
1372 branchcode => $branch,
1375 my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1377 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1378 my $issue = AddIssue( $borrower, $item->barcode );
1381 issue_id => $issue->id(),
1382 itemnumber => $item->itemnumber,
1383 borrowernumber => $borrowernumber,
1389 my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1390 my $count = $hr->{count};
1392 is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1395 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1396 $dbh->do('DELETE FROM issues');
1397 $dbh->do('DELETE FROM items');
1398 $dbh->do('DELETE FROM circulation_rules');
1399 Koha::CirculationRules->set_rules(
1401 categorycode => undef,
1403 branchcode => undef,
1405 reservesallowed => 25,
1407 lengthunit => 'days',
1408 renewalsallowed => 1,
1410 norenewalbefore => undef,
1418 my $biblio = $builder->build_sample_biblio();
1420 my $item_1 = $builder->build_sample_item(
1422 biblionumber => $biblio->biblionumber,
1423 library => $library2->{branchcode},
1428 my $item_2= $builder->build_sample_item(
1430 biblionumber => $biblio->biblionumber,
1431 library => $library2->{branchcode},
1436 my $borrowernumber1 = Koha::Patron->new({
1437 firstname => 'Kyle',
1439 categorycode => $patron_category->{categorycode},
1440 branchcode => $library2->{branchcode},
1441 })->store->borrowernumber;
1442 my $borrowernumber2 = Koha::Patron->new({
1443 firstname => 'Chelsea',
1445 categorycode => $patron_category->{categorycode},
1446 branchcode => $library2->{branchcode},
1447 })->store->borrowernumber;
1449 my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
1450 my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
1452 my $issue = AddIssue( $borrower1, $item_1->barcode );
1454 my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1455 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1459 branchcode => $library2->{branchcode},
1460 borrowernumber => $borrowernumber2,
1461 biblionumber => $biblio->biblionumber,
1466 Koha::CirculationRules->set_rules(
1468 categorycode => undef,
1470 branchcode => undef,
1476 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1477 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1478 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1480 Koha::CirculationRules->set_rules(
1482 categorycode => undef,
1484 branchcode => undef,
1490 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1491 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1492 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1494 Koha::CirculationRules->set_rules(
1496 categorycode => undef,
1498 branchcode => undef,
1504 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1505 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1506 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1508 Koha::CirculationRules->set_rules(
1510 categorycode => undef,
1512 branchcode => undef,
1518 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1519 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1520 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1522 # Setting item not checked out to be not for loan but holdable
1523 $item_2->notforloan(-1)->store;
1525 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1526 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' );
1530 # Don't allow renewing onsite checkout
1531 my $branch = $library->{branchcode};
1533 #Create another record
1534 my $biblio = $builder->build_sample_biblio();
1536 my $item = $builder->build_sample_item(
1538 biblionumber => $biblio->biblionumber,
1544 my $borrowernumber = Koha::Patron->new({
1547 categorycode => $patron_category->{categorycode},
1548 branchcode => $branch,
1549 })->store->borrowernumber;
1551 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1553 my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
1554 my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $item->itemnumber );
1555 is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
1556 is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
1560 my $library = $builder->build({ source => 'Branch' });
1562 my $biblio = $builder->build_sample_biblio();
1564 my $item = $builder->build_sample_item(
1566 biblionumber => $biblio->biblionumber,
1567 library => $library->{branchcode},
1572 my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
1574 my $issue = AddIssue( $patron, $item->barcode );
1577 issue_id => $issue->id(),
1578 itemnumber => $item->itemnumber,
1579 borrowernumber => $patron->{borrowernumber},
1586 issue_id => $issue->id(),
1587 itemnumber => $item->itemnumber,
1588 borrowernumber => $patron->{borrowernumber},
1593 is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1596 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1599 my $homebranch = $builder->build( { source => 'Branch' } );
1600 my $holdingbranch = $builder->build( { source => 'Branch' } );
1601 my $otherbranch = $builder->build( { source => 'Branch' } );
1602 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1603 my $patron_2 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1605 my $item = $builder->build_sample_item(
1607 homebranch => $homebranch->{branchcode},
1608 holdingbranch => $holdingbranch->{branchcode},
1612 set_userenv($holdingbranch);
1614 my $issue = AddIssue( $patron_1->unblessed, $item->{barcode} );
1615 is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
1617 my ( $error, $question, $alerts );
1619 # AllowReturnToBranch == anywhere
1620 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1621 ## Test that unknown barcodes don't generate internal server errors
1622 set_userenv($homebranch);
1623 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
1624 ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
1625 ## Can be issued from homebranch
1626 set_userenv($homebranch);
1627 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1628 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1629 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1630 ## Can be issued from holdingbranch
1631 set_userenv($holdingbranch);
1632 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1633 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1634 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1635 ## Can be issued from another branch
1636 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1637 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1638 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1640 # AllowReturnToBranch == holdingbranch
1641 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1642 ## Cannot be issued from homebranch
1643 set_userenv($homebranch);
1644 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1645 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1646 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1647 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
1648 ## Can be issued from holdinbranch
1649 set_userenv($holdingbranch);
1650 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1651 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1652 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1653 ## Cannot be issued from another branch
1654 set_userenv($otherbranch);
1655 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1656 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1657 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1658 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
1660 # AllowReturnToBranch == homebranch
1661 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1662 ## Can be issued from holdinbranch
1663 set_userenv($homebranch);
1664 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1665 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1666 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1667 ## Cannot be issued from holdinbranch
1668 set_userenv($holdingbranch);
1669 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1670 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1671 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1672 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1673 ## Cannot be issued from holdinbranch
1674 set_userenv($otherbranch);
1675 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1676 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1677 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1678 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1680 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1683 subtest 'AddIssue & AllowReturnToBranch' => sub {
1686 my $homebranch = $builder->build( { source => 'Branch' } );
1687 my $holdingbranch = $builder->build( { source => 'Branch' } );
1688 my $otherbranch = $builder->build( { source => 'Branch' } );
1689 my $patron_1 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1690 my $patron_2 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1692 my $item = $builder->build_sample_item(
1694 homebranch => $homebranch->{branchcode},
1695 holdingbranch => $holdingbranch->{branchcode},
1699 set_userenv($holdingbranch);
1701 my $ref_issue = 'Koha::Checkout';
1702 my $issue = AddIssue( $patron_1, $item->{barcode} );
1704 my ( $error, $question, $alerts );
1706 # AllowReturnToBranch == homebranch
1707 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1708 ## Can be issued from homebranch
1709 set_userenv($homebranch);
1710 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
1711 set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1712 ## Can be issued from holdinbranch
1713 set_userenv($holdingbranch);
1714 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
1715 set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1716 ## Can be issued from another branch
1717 set_userenv($otherbranch);
1718 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
1719 set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1721 # AllowReturnToBranch == holdinbranch
1722 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1723 ## Cannot be issued from homebranch
1724 set_userenv($homebranch);
1725 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
1726 ## Can be issued from holdingbranch
1727 set_userenv($holdingbranch);
1728 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
1729 set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1730 ## Cannot be issued from another branch
1731 set_userenv($otherbranch);
1732 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
1734 # AllowReturnToBranch == homebranch
1735 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1736 ## Can be issued from homebranch
1737 set_userenv($homebranch);
1738 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
1739 set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1740 ## Cannot be issued from holdinbranch
1741 set_userenv($holdingbranch);
1742 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
1743 ## Cannot be issued from another branch
1744 set_userenv($otherbranch);
1745 is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
1746 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1749 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1752 my $library = $builder->build( { source => 'Branch' } );
1753 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1754 my $item_1 = $builder->build_sample_item(
1756 library => $library->{branchcode},
1759 my $item_2 = $builder->build_sample_item(
1761 library => $library->{branchcode},
1765 my ( $error, $question, $alerts );
1767 # Patron cannot issue item_1, they have overdues
1768 my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1769 my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, $yesterday ); # Add an overdue
1771 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1772 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1773 is( keys(%$error) + keys(%$alerts), 0, 'No key for error and alert' . str($error, $question, $alerts) );
1774 is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
1776 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1777 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1778 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1779 is( $error->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
1781 # Patron cannot issue item_1, they are debarred
1782 my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1783 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
1784 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1785 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1786 is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
1788 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
1789 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1790 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1791 is( $error->{USERBLOCKEDNOENDDATE}, '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
1794 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
1797 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1798 my $patron_category_x = $builder->build_object(
1800 class => 'Koha::Patron::Categories',
1801 value => { category_type => 'X' }
1804 my $patron = $builder->build_object(
1806 class => 'Koha::Patrons',
1808 categorycode => $patron_category_x->categorycode,
1809 gonenoaddress => undef,
1816 my $item_1 = $builder->build_sample_item(
1818 library => $library->{branchcode},
1822 my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->{barcode} );
1823 is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
1825 # TODO There are other tests to provide here
1828 subtest 'MultipleReserves' => sub {
1831 my $biblio = $builder->build_sample_biblio();
1833 my $branch = $library2->{branchcode};
1835 my $item_1 = $builder->build_sample_item(
1837 biblionumber => $biblio->biblionumber,
1839 replacementprice => 12.00,
1844 my $item_2 = $builder->build_sample_item(
1846 biblionumber => $biblio->biblionumber,
1848 replacementprice => 12.00,
1855 my $resdate = undef;
1856 my $expdate = undef;
1858 my $checkitem = undef;
1861 my %renewing_borrower_data = (
1862 firstname => 'John',
1863 surname => 'Renewal',
1864 categorycode => $patron_category->{categorycode},
1865 branchcode => $branch,
1867 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
1868 my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
1869 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
1870 my $datedue = dt_from_string( $issue->date_due() );
1871 is (defined $issue->date_due(), 1, "item 1 checked out");
1872 my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
1874 my %reserving_borrower_data1 = (
1875 firstname => 'Katrin',
1876 surname => 'Reservation',
1877 categorycode => $patron_category->{categorycode},
1878 branchcode => $branch,
1880 my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
1883 branchcode => $branch,
1884 borrowernumber => $reserving_borrowernumber1,
1885 biblionumber => $biblio->biblionumber,
1886 priority => $priority,
1887 reservation_date => $resdate,
1888 expiration_date => $expdate,
1890 itemnumber => $checkitem,
1895 my %reserving_borrower_data2 = (
1896 firstname => 'Kirk',
1897 surname => 'Reservation',
1898 categorycode => $patron_category->{categorycode},
1899 branchcode => $branch,
1901 my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
1904 branchcode => $branch,
1905 borrowernumber => $reserving_borrowernumber2,
1906 biblionumber => $biblio->biblionumber,
1907 priority => $priority,
1908 reservation_date => $resdate,
1909 expiration_date => $expdate,
1911 itemnumber => $checkitem,
1917 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1918 is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
1921 my $item_3 = $builder->build_sample_item(
1923 biblionumber => $biblio->biblionumber,
1925 replacementprice => 12.00,
1931 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1932 is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
1936 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
1939 my $library = $builder->build( { source => 'Branch' } );
1940 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1942 my $biblionumber = $builder->build_sample_biblio(
1944 branchcode => $library->{branchcode},
1947 my $item_1 = $builder->build_sample_item(
1949 biblionumber => $biblionumber,
1950 library => $library->{branchcode},
1954 my $item_2 = $builder->build_sample_item(
1956 biblionumber => $biblionumber,
1957 library => $library->{branchcode},
1961 my ( $error, $question, $alerts );
1962 my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, dt_from_string->add( days => 1 ) );
1964 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1965 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1967 { error => $error, alerts => $alerts },
1968 { error => {}, alerts => {} },
1969 'No error or alert should be raised'
1971 is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
1973 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1974 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1976 { error => $error, question => $question, alerts => $alerts },
1977 { error => {}, question => {}, alerts => {} },
1978 'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
1981 # Add a subscription
1982 Koha::Subscription->new({ biblionumber => $biblionumber })->store;
1984 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1985 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1987 { error => $error, question => $question, alerts => $alerts },
1988 { error => {}, question => {}, alerts => {} },
1989 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
1992 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1993 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1995 { error => $error, question => $question, alerts => $alerts },
1996 { error => {}, question => {}, alerts => {} },
1997 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2001 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
2004 my $library = $builder->build( { source => 'Branch' } );
2005 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2008 my $biblionumber = $builder->build_sample_biblio(
2010 branchcode => $library->{branchcode},
2013 my $item_1 = $builder->build_sample_item(
2015 biblionumber => $biblionumber,
2016 library => $library->{branchcode},
2019 my $item_2 = $builder->build_sample_item(
2021 biblionumber => $biblionumber,
2022 library => $library->{branchcode},
2026 # And the circulation rule
2027 Koha::CirculationRules->search->delete;
2028 Koha::CirculationRules->set_rules(
2030 categorycode => undef,
2032 branchcode => undef,
2035 firstremind => 1, # 1 day of grace
2036 finedays => 2, # 2 days of fine per day of overdue
2037 lengthunit => 'days',
2042 # Patron cannot issue item_1, they have overdues
2043 my $now = dt_from_string;
2044 my $five_days_ago = $now->clone->subtract( days => 5 );
2045 my $ten_days_ago = $now->clone->subtract( days => 10 );
2046 AddIssue( $patron, $item_1->{barcode}, $five_days_ago ); # Add an overdue
2047 AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
2048 ; # Add another overdue
2050 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2051 AddReturn( $item_1->{barcode}, $library->{branchcode}, undef, $now );
2052 my $debarments = Koha::Patron::Debarments::GetDebarments(
2053 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2054 is( scalar(@$debarments), 1 );
2056 # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2057 # Same for the others
2058 my $expected_expiration = output_pref(
2060 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2061 dateformat => 'sql',
2065 is( $debarments->[0]->{expiration}, $expected_expiration );
2067 AddReturn( $item_2->{barcode}, $library->{branchcode}, undef, $now );
2068 $debarments = Koha::Patron::Debarments::GetDebarments(
2069 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2070 is( scalar(@$debarments), 1 );
2071 $expected_expiration = output_pref(
2073 dt => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2074 dateformat => 'sql',
2078 is( $debarments->[0]->{expiration}, $expected_expiration );
2080 Koha::Patron::Debarments::DelUniqueDebarment(
2081 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2083 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2084 AddIssue( $patron, $item_1->{barcode}, $five_days_ago ); # Add an overdue
2085 AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
2086 ; # Add another overdue
2087 AddReturn( $item_1->{barcode}, $library->{branchcode}, undef, $now );
2088 $debarments = Koha::Patron::Debarments::GetDebarments(
2089 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2090 is( scalar(@$debarments), 1 );
2091 $expected_expiration = output_pref(
2093 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2094 dateformat => 'sql',
2098 is( $debarments->[0]->{expiration}, $expected_expiration );
2100 AddReturn( $item_2->{barcode}, $library->{branchcode}, undef, $now );
2101 $debarments = Koha::Patron::Debarments::GetDebarments(
2102 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2103 is( scalar(@$debarments), 1 );
2104 $expected_expiration = output_pref(
2106 dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
2107 dateformat => 'sql',
2111 is( $debarments->[0]->{expiration}, $expected_expiration );
2114 subtest 'AddReturn + suspension_chargeperiod' => sub {
2117 my $library = $builder->build( { source => 'Branch' } );
2118 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2120 my $biblionumber = $builder->build_sample_biblio(
2122 branchcode => $library->{branchcode},
2125 my $item_1 = $builder->build_sample_item(
2127 biblionumber => $biblionumber,
2128 library => $library->{branchcode},
2132 # And the issuing rule
2133 Koha::CirculationRules->search->delete;
2134 Koha::CirculationRules->set_rules(
2136 categorycode => '*',
2141 firstremind => 0, # 0 day of grace
2142 finedays => 2, # 2 days of fine per day of overdue
2143 suspension_chargeperiod => 1,
2144 lengthunit => 'days',
2149 my $now = dt_from_string;
2150 my $five_days_ago = $now->clone->subtract( days => 5 );
2151 # We want to charge 2 days every day, without grace
2152 # With 5 days of overdue: 5 * Z
2153 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2154 test_debarment_on_checkout(
2157 library => $library,
2159 due_date => $five_days_ago,
2160 expiration_date => $expected_expiration,
2164 # Same with undef firstremind
2165 Koha::CirculationRules->search->delete;
2166 Koha::CirculationRules->set_rules(
2168 categorycode => '*',
2173 firstremind => undef, # 0 day of grace
2174 finedays => 2, # 2 days of fine per day of overdue
2175 suspension_chargeperiod => 1,
2176 lengthunit => 'days',
2181 my $now = dt_from_string;
2182 my $five_days_ago = $now->clone->subtract( days => 5 );
2183 # We want to charge 2 days every day, without grace
2184 # With 5 days of overdue: 5 * Z
2185 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2186 test_debarment_on_checkout(
2189 library => $library,
2191 due_date => $five_days_ago,
2192 expiration_date => $expected_expiration,
2196 # We want to charge 2 days every 2 days, without grace
2197 # With 5 days of overdue: (5 * 2) / 2
2198 Koha::CirculationRules->set_rule(
2200 categorycode => undef,
2201 branchcode => undef,
2203 rule_name => 'suspension_chargeperiod',
2208 $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
2209 test_debarment_on_checkout(
2212 library => $library,
2214 due_date => $five_days_ago,
2215 expiration_date => $expected_expiration,
2219 # We want to charge 2 days every 3 days, with 1 day of grace
2220 # With 5 days of overdue: ((5-1) / 3 ) * 2
2221 Koha::CirculationRules->set_rules(
2223 categorycode => undef,
2224 branchcode => undef,
2227 suspension_chargeperiod => 3,
2232 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
2233 test_debarment_on_checkout(
2236 library => $library,
2238 due_date => $five_days_ago,
2239 expiration_date => $expected_expiration,
2243 # Use finesCalendar to know if holiday must be skipped to calculate the due date
2244 # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
2245 Koha::CirculationRules->set_rules(
2247 categorycode => undef,
2248 branchcode => undef,
2252 suspension_chargeperiod => 1,
2257 t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
2258 t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
2260 # Adding a holiday 2 days ago
2261 my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
2262 my $two_days_ago = $now->clone->subtract( days => 2 );
2263 $calendar->insert_single_holiday(
2264 day => $two_days_ago->day,
2265 month => $two_days_ago->month,
2266 year => $two_days_ago->year,
2267 title => 'holidayTest-2d',
2268 description => 'holidayDesc 2 days ago'
2270 # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
2271 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
2272 test_debarment_on_checkout(
2275 library => $library,
2277 due_date => $five_days_ago,
2278 expiration_date => $expected_expiration,
2282 # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
2283 my $two_days_ahead = $now->clone->add( days => 2 );
2284 $calendar->insert_single_holiday(
2285 day => $two_days_ahead->day,
2286 month => $two_days_ahead->month,
2287 year => $two_days_ahead->year,
2288 title => 'holidayTest+2d',
2289 description => 'holidayDesc 2 days ahead'
2292 # Same as above, but we should skip D+2
2293 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
2294 test_debarment_on_checkout(
2297 library => $library,
2299 due_date => $five_days_ago,
2300 expiration_date => $expected_expiration,
2304 # Adding another holiday, day of expiration date
2305 my $expected_expiration_dt = dt_from_string($expected_expiration);
2306 $calendar->insert_single_holiday(
2307 day => $expected_expiration_dt->day,
2308 month => $expected_expiration_dt->month,
2309 year => $expected_expiration_dt->year,
2310 title => 'holidayTest_exp',
2311 description => 'holidayDesc on expiration date'
2313 # Expiration date will be the day after
2314 test_debarment_on_checkout(
2317 library => $library,
2319 due_date => $five_days_ago,
2320 expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
2324 test_debarment_on_checkout(
2327 library => $library,
2329 return_date => $now->clone->add(days => 5),
2330 expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
2334 test_debarment_on_checkout(
2337 library => $library,
2339 due_date => $now->clone->add(days => 1),
2340 return_date => $now->clone->add(days => 5),
2341 expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
2347 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
2350 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2351 my $patron1 = $builder->build_object(
2353 class => 'Koha::Patrons',
2355 library => $library->branchcode,
2356 categorycode => $patron_category->{categorycode}
2360 my $patron2 = $builder->build_object(
2362 class => 'Koha::Patrons',
2364 library => $library->branchcode,
2365 categorycode => $patron_category->{categorycode}
2370 t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
2372 my $item = $builder->build_sample_item(
2374 library => $library->branchcode,
2378 my ( $error, $question, $alerts );
2379 my $issue = AddIssue( $patron1->unblessed, $item->{barcode} );
2381 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2382 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->{barcode} );
2383 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' );
2385 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
2386 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->{barcode} );
2387 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' );
2389 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2393 subtest 'AddReturn | is_overdue' => sub {
2396 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
2397 t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
2398 t::lib::Mocks::mock_preference('finesMode', 'production');
2399 t::lib::Mocks::mock_preference('MaxFine', '100');
2401 my $library = $builder->build( { source => 'Branch' } );
2402 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2403 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2404 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
2406 my $item = $builder->build_sample_item(
2408 library => $library->{branchcode},
2409 replacementprice => 7
2413 Koha::CirculationRules->search->delete;
2414 Koha::CirculationRules->set_rules(
2416 categorycode => undef,
2418 branchcode => undef,
2421 lengthunit => 'days',
2422 fine => 1, # Charge 1 every day of overdue
2428 my $now = dt_from_string;
2429 my $one_day_ago = $now->clone->subtract( days => 1 );
2430 my $two_days_ago = $now->clone->subtract( days => 2 );
2431 my $five_days_ago = $now->clone->subtract( days => 5 );
2432 my $ten_days_ago = $now->clone->subtract( days => 10 );
2433 $patron = Koha::Patrons->find( $patron->{borrowernumber} );
2435 # No return date specified, today will be used => 10 days overdue charged
2436 AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2437 AddReturn( $item->{barcode}, $library->{branchcode} );
2438 is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
2439 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2441 # specify return date 5 days before => no overdue charged
2442 AddIssue( $patron->unblessed, $item->{barcode}, $five_days_ago ); # date due was 5d ago
2443 AddReturn( $item->{barcode}, $library->{branchcode}, undef, $ten_days_ago );
2444 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2445 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2447 # specify return date 5 days later => 5 days overdue charged
2448 AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2449 AddReturn( $item->{barcode}, $library->{branchcode}, undef, $five_days_ago );
2450 is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
2451 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2453 # specify return date 5 days later, specify exemptfine => no overdue charge
2454 AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2455 AddReturn( $item->{barcode}, $library->{branchcode}, 1, $five_days_ago );
2456 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2457 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2459 subtest 'bug 22877' => sub {
2463 my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2465 # Fake fines cronjob on this checkout
2467 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2468 $ten_days_ago, $now );
2471 issue_id => $issue->issue_id,
2472 itemnumber => $item->{itemnumber},
2473 borrowernumber => $patron->borrowernumber,
2475 due => output_pref($ten_days_ago)
2478 is( int( $patron->account->balance() ),
2479 10, "Overdue fine of 10 days overdue" );
2481 # Fake longoverdue with charge and not marking returned
2482 LostItem( $item->{itemnumber}, 'cronjob', 0 );
2483 is( int( $patron->account->balance() ),
2484 17, "Lost fine of 7 plus 10 days overdue" );
2486 # Now we return it today
2487 AddReturn( $item->{barcode}, $library->{branchcode} );
2488 is( int( $patron->account->balance() ),
2489 17, "Should have a single 10 days overdue fine and lost charge" );
2492 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2495 subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
2499 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2501 my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $one_day_ago ); # date due was 1d ago
2503 # Fake fines cronjob on this checkout
2505 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2506 $one_day_ago, $now );
2509 issue_id => $issue->issue_id,
2510 itemnumber => $item->{itemnumber},
2511 borrowernumber => $patron->borrowernumber,
2513 due => output_pref($one_day_ago)
2516 is( int( $patron->account->balance() ),
2517 1, "Overdue fine of 1 day overdue" );
2519 # Backdated return (dropbox mode example - charge should be removed)
2520 AddReturn( $item->{barcode}, $library->{branchcode}, 1, $one_day_ago );
2521 is( int( $patron->account->balance() ),
2522 0, "Overdue fine should be annulled" );
2523 my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2524 is( $lines->count, 0, "Overdue fine accountline has been removed");
2526 $issue = AddIssue( $patron->unblessed, $item->{barcode}, $two_days_ago ); # date due was 2d ago
2528 # Fake fines cronjob on this checkout
2530 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2531 $two_days_ago, $now );
2534 issue_id => $issue->issue_id,
2535 itemnumber => $item->{itemnumber},
2536 borrowernumber => $patron->borrowernumber,
2538 due => output_pref($one_day_ago)
2541 is( int( $patron->account->balance() ),
2542 2, "Overdue fine of 2 days overdue" );
2544 # Payment made against fine
2545 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2546 my $debit = $lines->next;
2547 my $credit = $patron->account->add_credit(
2551 interface => 'test',
2555 { debits => [ $debit ], offset_type => 'Payment' } );
2557 is( int( $patron->account->balance() ),
2558 0, "Overdue fine should be paid off" );
2559 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2560 is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
2561 my $line = $lines->next;
2562 is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
2563 is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
2565 # Backdated return (dropbox mode example - charge should be removed)
2566 AddReturn( $item->{barcode}, $library->{branchcode}, undef, $one_day_ago );
2567 is( int( $patron->account->balance() ),
2568 -1, "Refund credit has been applied" );
2569 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
2570 is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
2572 $line = $lines->next;
2573 is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
2574 is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
2575 is($line->status,'RETURNED', "Overdue fine is fixed");
2576 $line = $lines->next;
2577 is($line->amount+0,-2, "Original payment amount remains as 2");
2578 is($line->amountoutstanding+0,0, "Original payment remains applied");
2579 $line = $lines->next;
2580 is($line->amount+0,-1, "Refund amount correctly set to 1");
2581 is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
2584 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2587 subtest 'bug 25417 | backdated return + exemptfine' => sub {
2591 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2593 my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $one_day_ago ); # date due was 1d ago
2595 # Fake fines cronjob on this checkout
2597 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2598 $one_day_ago, $now );
2601 issue_id => $issue->issue_id,
2602 itemnumber => $item->{itemnumber},
2603 borrowernumber => $patron->borrowernumber,
2605 due => output_pref($one_day_ago)
2608 is( int( $patron->account->balance() ),
2609 1, "Overdue fine of 1 day overdue" );
2611 # Backdated return (dropbox mode example - charge should no longer exist)
2612 AddReturn( $item->{barcode}, $library->{branchcode}, 1, $one_day_ago );
2613 is( int( $patron->account->balance() ),
2614 0, "Overdue fine should be annulled" );
2617 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2620 subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
2623 t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
2625 my $due_date = dt_from_string;
2626 my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $due_date );
2631 issue_id => $issue->issue_id,
2632 itemnumber => $item->{itemnumber},
2633 borrowernumber => $patron->borrowernumber,
2635 due => output_pref($due_date)
2638 is( $patron->account->balance(),
2639 0.25, 'Overdue fine of $0.25 recorded' );
2641 # Backdate return to exact due date and time
2642 my ( undef, $message ) =
2643 AddReturn( $item->{barcode}, $library->{branchcode},
2647 Koha::Account::Lines->find( { issue_id => $issue->id } );
2648 ok( !$accountline, 'accountline removed as expected' );
2651 $issue = AddIssue( $patron->unblessed, $item->{barcode}, $due_date );
2656 issue_id => $issue->issue_id,
2657 itemnumber => $item->{itemnumber},
2658 borrowernumber => $patron->borrowernumber,
2660 due => output_pref($due_date)
2663 is( $patron->account->balance(),
2664 0.25, 'Overdue fine of $0.25 recorded' );
2666 # Partial pay accruing fine
2667 my $lines = Koha::Account::Lines->search(
2669 borrowernumber => $patron->borrowernumber,
2670 issue_id => $issue->id
2673 my $debit = $lines->next;
2674 my $credit = $patron->account->add_credit(
2678 interface => 'test',
2681 $credit->apply( { debits => [$debit], offset_type => 'Payment' } );
2683 is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
2685 # Backdate return to exact due date and time
2686 ( undef, $message ) =
2687 AddReturn( $item->{barcode}, $library->{branchcode},
2690 $lines = Koha::Account::Lines->search(
2692 borrowernumber => $patron->borrowernumber,
2693 issue_id => $issue->id
2696 $accountline = $lines->next;
2697 is( $accountline->amountoutstanding + 0,
2698 0, 'Partially paid fee amount outstanding was reduced to 0' );
2699 is( $accountline->amount + 0,
2700 0, 'Partially paid fee amount was reduced to 0' );
2701 is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
2704 Koha::Account::Lines->search(
2705 { borrowernumber => $patron->borrowernumber } )->delete;
2709 subtest '_FixAccountForLostAndFound' => sub {
2713 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
2714 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
2716 my $processfee_amount = 20;
2717 my $replacement_amount = 99.00;
2718 my $item_type = $builder->build_object(
2719 { class => 'Koha::ItemTypes',
2721 notforloan => undef,
2723 defaultreplacecost => undef,
2724 processfee => $processfee_amount,
2725 rentalcharge_daily => 0,
2729 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2731 my $biblio = $builder->build_sample_biblio({ author => 'Hall, Daria' });
2733 subtest 'Full write-off tests' => sub {
2737 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2738 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2739 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
2741 my $item = $builder->build_sample_item(
2743 biblionumber => $biblio->biblionumber,
2744 library => $library->branchcode,
2745 replacementprice => $replacement_amount,
2746 itype => $item_type->itemtype,
2750 AddIssue( $patron->unblessed, $item->barcode );
2752 # Simulate item marked as lost
2753 $item->itemlost(3)->store;
2754 LostItem( $item->itemnumber, 1 );
2756 my $processing_fee_lines = Koha::Account::Lines->search(
2757 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2758 is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2759 my $processing_fee_line = $processing_fee_lines->next;
2760 is( $processing_fee_line->amount + 0,
2761 $processfee_amount, 'The right PROCESSING amount is generated' );
2762 is( $processing_fee_line->amountoutstanding + 0,
2763 $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2765 my $lost_fee_lines = Koha::Account::Lines->search(
2766 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2767 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2768 my $lost_fee_line = $lost_fee_lines->next;
2769 is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2770 is( $lost_fee_line->amountoutstanding + 0,
2771 $replacement_amount, 'The right LOST amountoutstanding is generated' );
2772 is( $lost_fee_line->status,
2773 undef, 'The LOST status was not set' );
2775 my $account = $patron->account;
2776 my $debts = $account->outstanding_debits;
2778 # Write off the debt
2779 my $credit = $account->add_credit(
2780 { amount => $account->balance,
2782 interface => 'test',
2785 $credit->apply( { debits => [ $debts->as_list ], offset_type => 'Writeoff' } );
2787 my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2788 is( $credit_return_id, undef, 'No LOST_FOUND account line added' );
2790 $lost_fee_line->discard_changes; # reload from DB
2791 is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2792 is( $lost_fee_line->debit_type_code,
2793 'LOST', 'Lost fee now still has account type of LOST' );
2794 is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
2796 is( $patron->account->balance, -0, 'The patron balance is 0, everything was written off' );
2799 subtest 'Full payment tests' => sub {
2803 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2805 my $item = $builder->build_sample_item(
2807 biblionumber => $biblio->biblionumber,
2808 library => $library->branchcode,
2809 replacementprice => $replacement_amount,
2810 itype => $item_type->itemtype
2814 AddIssue( $patron->unblessed, $item->barcode );
2816 # Simulate item marked as lost
2817 $item->itemlost(1)->store;
2818 LostItem( $item->itemnumber, 1 );
2820 my $processing_fee_lines = Koha::Account::Lines->search(
2821 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2822 is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2823 my $processing_fee_line = $processing_fee_lines->next;
2824 is( $processing_fee_line->amount + 0,
2825 $processfee_amount, 'The right PROCESSING amount is generated' );
2826 is( $processing_fee_line->amountoutstanding + 0,
2827 $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2829 my $lost_fee_lines = Koha::Account::Lines->search(
2830 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2831 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2832 my $lost_fee_line = $lost_fee_lines->next;
2833 is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2834 is( $lost_fee_line->amountoutstanding + 0,
2835 $replacement_amount, 'The right LOST amountountstanding is generated' );
2837 my $account = $patron->account;
2838 my $debts = $account->outstanding_debits;
2840 # Write off the debt
2841 my $credit = $account->add_credit(
2842 { amount => $account->balance,
2844 interface => 'test',
2847 $credit->apply( { debits => [ $debts->as_list ], offset_type => 'Payment' } );
2849 my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2850 my $credit_return = Koha::Account::Lines->find($credit_return_id);
2852 is( $credit_return->credit_type_code, 'LOST_FOUND', 'An account line of type LOST_FOUND is added' );
2853 is( $credit_return->amount + 0,
2854 -99.00, 'The account line of type LOST_FOUND has an amount of -99' );
2855 is( $credit_return->amountoutstanding + 0,
2856 -99.00, 'The account line of type LOST_FOUND has an amountoutstanding of -99' );
2858 $lost_fee_line->discard_changes;
2859 is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2860 is( $lost_fee_line->debit_type_code,
2861 'LOST', 'Lost fee now still has account type of LOST' );
2862 is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
2864 is( $patron->account->balance,
2865 -99, 'The patron balance is -99, a credit that equals the lost fee payment' );
2868 subtest 'Test without payment or write off' => sub {
2872 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2874 my $item = $builder->build_sample_item(
2876 biblionumber => $biblio->biblionumber,
2877 library => $library->branchcode,
2878 replacementprice => 23.00,
2879 replacementprice => $replacement_amount,
2880 itype => $item_type->itemtype
2884 AddIssue( $patron->unblessed, $item->barcode );
2886 # Simulate item marked as lost
2887 $item->itemlost(3)->store;
2888 LostItem( $item->itemnumber, 1 );
2890 my $processing_fee_lines = Koha::Account::Lines->search(
2891 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2892 is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2893 my $processing_fee_line = $processing_fee_lines->next;
2894 is( $processing_fee_line->amount + 0,
2895 $processfee_amount, 'The right PROCESSING amount is generated' );
2896 is( $processing_fee_line->amountoutstanding + 0,
2897 $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2899 my $lost_fee_lines = Koha::Account::Lines->search(
2900 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2901 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2902 my $lost_fee_line = $lost_fee_lines->next;
2903 is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2904 is( $lost_fee_line->amountoutstanding + 0,
2905 $replacement_amount, 'The right LOST amountountstanding is generated' );
2907 my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2908 my $credit_return = Koha::Account::Lines->find($credit_return_id);
2910 is( $credit_return->credit_type_code, 'LOST_FOUND', 'An account line of type LOST_FOUND is added' );
2911 is( $credit_return->amount + 0, -99.00, 'The account line of type LOST_FOUND has an amount of -99' );
2912 is( $credit_return->amountoutstanding + 0, 0, 'The account line of type LOST_FOUND has an amountoutstanding of 0' );
2914 $lost_fee_line->discard_changes;
2915 is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2916 is( $lost_fee_line->debit_type_code,
2917 'LOST', 'Lost fee now still has account type of LOST' );
2918 is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
2920 is( $patron->account->balance, 20, 'The patron balance is 20, still owes the processing fee' );
2923 subtest 'Test with partial payement and write off, and remaining debt' => sub {
2927 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2928 my $item = $builder->build_sample_item(
2930 biblionumber => $biblio->biblionumber,
2931 library => $library->branchcode,
2932 replacementprice => $replacement_amount,
2933 itype => $item_type->itemtype
2937 AddIssue( $patron->unblessed, $item->barcode );
2939 # Simulate item marked as lost
2940 $item->itemlost(1)->store;
2941 LostItem( $item->itemnumber, 1 );
2943 my $processing_fee_lines = Koha::Account::Lines->search(
2944 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2945 is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2946 my $processing_fee_line = $processing_fee_lines->next;
2947 is( $processing_fee_line->amount + 0,
2948 $processfee_amount, 'The right PROCESSING amount is generated' );
2949 is( $processing_fee_line->amountoutstanding + 0,
2950 $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2952 my $lost_fee_lines = Koha::Account::Lines->search(
2953 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2954 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2955 my $lost_fee_line = $lost_fee_lines->next;
2956 is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2957 is( $lost_fee_line->amountoutstanding + 0,
2958 $replacement_amount, 'The right LOST amountountstanding is generated' );
2960 my $account = $patron->account;
2961 is( $account->balance, $processfee_amount + $replacement_amount, 'Balance is PROCESSING + L' );
2964 my $payment_amount = 27;
2965 my $payment = $account->add_credit(
2966 { amount => $payment_amount,
2968 interface => 'test',
2972 $payment->apply( { debits => [ $lost_fee_line ], offset_type => 'Payment' } );
2974 # Partially write off fee
2975 my $write_off_amount = 25;
2976 my $write_off = $account->add_credit(
2977 { amount => $write_off_amount,
2979 interface => 'test',
2982 $write_off->apply( { debits => [ $lost_fee_line ], offset_type => 'Writeoff' } );
2984 is( $account->balance,
2985 $processfee_amount + $replacement_amount - $payment_amount - $write_off_amount,
2986 'Payment and write off applied'
2989 # Store the amountoutstanding value
2990 $lost_fee_line->discard_changes;
2991 my $outstanding = $lost_fee_line->amountoutstanding;
2993 my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2994 my $credit_return = Koha::Account::Lines->find($credit_return_id);
2996 is( $account->balance, $processfee_amount - $payment_amount, 'Balance is PROCESSING - PAYMENT (LOST_FOUND)' );
2998 $lost_fee_line->discard_changes;
2999 is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
3000 is( $lost_fee_line->debit_type_code,
3001 'LOST', 'Lost fee now still has account type of LOST' );
3002 is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
3004 is( $credit_return->credit_type_code, 'LOST_FOUND', 'An account line of type LOST_FOUND is added' );
3005 is( $credit_return->amount + 0,
3006 ($payment_amount + $outstanding ) * -1,
3007 'The account line of type LOST_FOUND has an amount equal to the payment + outstanding'
3009 is( $credit_return->amountoutstanding + 0,
3010 $payment_amount * -1,
3011 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
3014 is( $account->balance,
3015 $processfee_amount - $payment_amount,
3016 'The patron balance is the difference between the PROCESSING and the credit'
3020 subtest 'Partial payement, existing debits and AccountAutoReconcile' => sub {
3024 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
3025 my $barcode = 'KD123456793';
3026 my $replacement_amount = 100;
3027 my $processfee_amount = 20;
3029 my $item_type = $builder->build_object(
3030 { class => 'Koha::ItemTypes',
3032 notforloan => undef,
3034 defaultreplacecost => undef,
3036 rentalcharge_daily => 0,
3040 my $item = Koha::Item->new(
3042 biblionumber => $biblio->biblionumber,
3043 homebranch => $library->branchcode,
3044 holdingbranch => $library->branchcode,
3045 barcode => $barcode,
3046 replacementprice => $replacement_amount,
3047 itype => $item_type->itemtype
3051 AddIssue( $patron->unblessed, $barcode );
3053 # Simulate item marked as lost
3054 $item->itemlost(1)->store;
3055 LostItem( $item->itemnumber, 1 );
3057 my $lost_fee_lines = Koha::Account::Lines->search(
3058 { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
3059 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
3060 my $lost_fee_line = $lost_fee_lines->next;
3061 is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
3062 is( $lost_fee_line->amountoutstanding + 0,
3063 $replacement_amount, 'The right LOST amountountstanding is generated' );
3065 my $account = $patron->account;
3066 is( $account->balance, $replacement_amount, 'Balance is L' );
3069 my $payment_amount = 27;
3070 my $payment = $account->add_credit(
3071 { amount => $payment_amount,
3073 interface => 'test',
3076 $payment->apply({ debits => [ $lost_fee_line ], offset_type => 'Payment' });
3078 is( $account->balance,
3079 $replacement_amount - $payment_amount,
3083 my $manual_debit_amount = 80;
3084 $account->add_debit( { amount => $manual_debit_amount, type => 'OVERDUE', interface =>'test' } );
3086 is( $account->balance, $manual_debit_amount + $replacement_amount - $payment_amount, 'Manual debit applied' );
3088 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
3090 my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
3091 my $credit_return = Koha::Account::Lines->find($credit_return_id);
3093 is( $account->balance, $manual_debit_amount - $payment_amount, 'Balance is PROCESSING - payment (LOST_FOUND)' );
3095 my $manual_debit = Koha::Account::Lines->search({ borrowernumber => $patron->id, debit_type_code => 'OVERDUE', status => 'UNRETURNED' })->next;
3096 is( $manual_debit->amountoutstanding + 0, $manual_debit_amount - $payment_amount, 'reconcile_balance was called' );
3100 subtest '_FixOverduesOnReturn' => sub {
3103 my $manager = $builder->build_object({ class => "Koha::Patrons" });
3104 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3106 my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
3108 my $branchcode = $library2->{branchcode};
3110 my $item = $builder->build_sample_item(
3112 biblionumber => $biblio->biblionumber,
3113 library => $branchcode,
3114 replacementprice => 99.00,
3119 my $patron = $builder->build( { source => 'Borrower' } );
3121 ## Start with basic call, should just close out the open fine
3122 my $accountline = Koha::Account::Line->new(
3124 borrowernumber => $patron->{borrowernumber},
3125 debit_type_code => 'OVERDUE',
3126 status => 'UNRETURNED',
3127 itemnumber => $item->itemnumber,
3129 amountoutstanding => 99.00,
3130 interface => 'test',
3134 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
3136 $accountline->_result()->discard_changes();
3138 is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
3139 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3140 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3142 ## Run again, with exemptfine enabled
3145 debit_type_code => 'OVERDUE',
3146 status => 'UNRETURNED',
3147 amountoutstanding => 99.00,
3151 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3153 $accountline->_result()->discard_changes();
3154 my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3156 is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
3157 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3158 is( $accountline->status, 'FORGIVEN', 'Open fine ( account type OVERDUE ) has been set to fine forgiven ( status FORGIVEN )');
3159 is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
3160 is( $offset->amount + 0, -99, "Amount of offset is correct" );
3161 my $credit = $offset->credit;
3162 is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
3163 is( $credit->amount + 0, -99, "Credit amount is set correctly" );
3164 is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
3166 # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
3169 debit_type_code => 'OVERDUE',
3170 status => 'UNRETURNED',
3171 amountoutstanding => 0.00,
3176 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3178 $accountline->_result()->discard_changes();
3179 $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3180 is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
3181 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3182 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3185 subtest '_FixAccountForLostAndFound returns undef if patron is deleted' => sub {
3188 my $manager = $builder->build_object({ class => "Koha::Patrons" });
3189 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3191 my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
3193 my $branchcode = $library2->{branchcode};
3195 my $item = $builder->build_sample_item(
3197 biblionumber => $biblio->biblionumber,
3198 library => $branchcode,
3199 replacementprice => 99.00,
3204 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
3206 ## Start with basic call, should just close out the open fine
3207 my $accountline = Koha::Account::Line->new(
3209 borrowernumber => $patron->id,
3210 debit_type_code => 'LOST',
3212 itemnumber => $item->itemnumber,
3214 amountoutstanding => 99.00,
3215 interface => 'test',
3221 my $return_value = C4::Circulation::_FixAccountForLostAndFound( $patron->id, $item->itemnumber );
3223 is( $return_value, undef, "_FixAccountForLostAndFound returns undef if patron is deleted" );
3227 subtest 'Set waiting flag' => sub {
3230 my $library_1 = $builder->build( { source => 'Branch' } );
3231 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3232 my $library_2 = $builder->build( { source => 'Branch' } );
3233 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3235 my $item = $builder->build_sample_item(
3237 library => $library_1->{branchcode},
3241 set_userenv( $library_2 );
3242 my $reserve_id = AddReserve(
3244 branchcode => $library_2->{branchcode},
3245 borrowernumber => $patron_2->{borrowernumber},
3246 biblionumber => $item->{biblionumber},
3248 itemnumber => $item->{itemnumber},
3252 set_userenv( $library_1 );
3253 my $do_transfer = 1;
3254 my ( $res, $rr ) = AddReturn( $item->{barcode}, $library_1->{branchcode} );
3255 ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
3256 my $hold = Koha::Holds->find( $reserve_id );
3257 is( $hold->found, 'T', 'Hold is in transit' );
3259 my ( $status ) = CheckReserves($item->{itemnumber});
3260 is( $status, 'Reserved', 'Hold is not waiting yet');
3262 set_userenv( $library_2 );
3264 AddReturn( $item->{barcode}, $library_2->{branchcode} );
3265 ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
3266 $hold = Koha::Holds->find( $reserve_id );
3267 is( $hold->found, 'W', 'Hold is waiting' );
3268 ( $status ) = CheckReserves($item->{itemnumber});
3269 is( $status, 'Waiting', 'Now the hold is waiting');
3271 #Bug 21944 - Waiting transfer checked in at branch other than pickup location
3272 set_userenv( $library_1 );
3273 (undef, my $messages, undef, undef ) = AddReturn ( $item->{barcode}, $library_1->{branchcode} );
3274 $hold = Koha::Holds->find( $reserve_id );
3275 is( $hold->found, undef, 'Hold is no longer marked waiting' );
3276 is( $hold->priority, 1, "Hold is now priority one again");
3277 is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
3278 is( $hold->itemnumber, $item->{itemnumber}, "Hold has retained its' itemnumber");
3279 is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
3280 is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
3281 is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
3284 subtest 'Cancel transfers on lost items' => sub {
3286 my $library_1 = $builder->build( { source => 'Branch' } );
3287 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3288 my $library_2 = $builder->build( { source => 'Branch' } );
3289 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3290 my $biblio = $builder->build_sample_biblio({branchcode => $library->{branchcode}});
3291 my $item = $builder->build_sample_item({
3292 biblionumber => $biblio->biblionumber,
3293 library => $library_1->{branchcode},
3296 set_userenv( $library_2 );
3297 my $reserve_id = AddReserve(
3299 branchcode => $library_2->{branchcode},
3300 borrowernumber => $patron_2->{borrowernumber},
3301 biblionumber => $item->biblionumber,
3303 itemnumber => $item->itemnumber,
3307 #Return book and add transfer
3308 set_userenv( $library_1 );
3309 my $do_transfer = 1;
3310 my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3311 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3312 C4::Circulation::transferbook( $library_2->{branchcode}, $item->barcode );
3313 my $hold = Koha::Holds->find( $reserve_id );
3314 is( $hold->found, 'T', 'Hold is in transit' );
3316 #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
3317 my ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3318 is( $tobranch, $library_2->{branchcode}, 'The transfer record exists in the branchtransfers table');
3319 my $itemcheck = Koha::Items->find($item->itemnumber);
3320 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Items holding branch is the transfers origin branch before it is marked as lost' );
3322 #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
3323 $item->itemlost(1)->store;
3324 LostItem( $item->itemnumber, 'test', 1 );
3325 ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3326 is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
3327 $itemcheck = Koha::Items->find($item->itemnumber);
3328 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
3331 subtest 'CanBookBeIssued | is_overdue' => sub {
3334 # Set a simple circ policy
3335 Koha::CirculationRules->set_rules(
3337 categorycode => undef,
3338 branchcode => undef,
3342 reservesallowed => 25,
3344 lengthunit => 'days',
3345 renewalsallowed => 1,
3347 norenewalbefore => undef,
3355 my $now = dt_from_string;
3356 my $five_days_go = output_pref({ dt => $now->clone->add( days => 5 ), dateonly => 1});
3357 my $ten_days_go = output_pref({ dt => $now->clone->add( days => 10), dateonly => 1 });
3358 my $library = $builder->build( { source => 'Branch' } );
3359 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
3361 my $item = $builder->build_sample_item(
3363 library => $library->{branchcode},
3367 my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $five_days_go ); # date due was 10d ago
3368 my $actualissue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
3369 is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
3370 my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->{barcode},$ten_days_go, undef, undef, undef);
3371 is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
3372 is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
3375 subtest 'ItemsDeniedRenewal preference' => sub {
3378 C4::Context->set_preference('ItemsDeniedRenewal','');
3380 my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
3381 Koha::CirculationRules->set_rules(
3383 categorycode => '*',
3385 branchcode => $idr_lib->branchcode,
3387 reservesallowed => 25,
3389 lengthunit => 'days',
3390 renewalsallowed => 10,
3392 norenewalbefore => undef,
3400 my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
3401 homebranch => $idr_lib->branchcode,
3405 itemcallnumber => undef,
3409 my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
3410 homebranch => $idr_lib->branchcode,
3413 location => 'NOPROC'
3417 my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
3418 branchcode => $idr_lib->branchcode,
3421 my $future = dt_from_string->add( days => 1 );
3422 my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3423 returndate => undef,
3426 borrowernumber => $idr_borrower->borrowernumber,
3427 itemnumber => $deny_book->itemnumber,
3428 onsite_checkout => 0,
3429 date_due => $future,
3432 my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3433 returndate => undef,
3436 borrowernumber => $idr_borrower->borrowernumber,
3437 itemnumber => $allow_book->itemnumber,
3438 onsite_checkout => 0,
3439 date_due => $future,
3445 my ( $idr_mayrenew, $idr_error ) =
3446 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3447 is( $idr_mayrenew, 1, 'Renewal allowed when no rules' );
3448 is( $idr_error, undef, 'Renewal allowed when no rules' );
3450 $idr_rules="withdrawn: [1]";
3452 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3453 ( $idr_mayrenew, $idr_error ) =
3454 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3455 is( $idr_mayrenew, 0, 'Renewal blocked when 1 rules (withdrawn)' );
3456 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 1 rule (withdrawn)' );
3457 ( $idr_mayrenew, $idr_error ) =
3458 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3459 is( $idr_mayrenew, 1, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3460 is( $idr_error, undef, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3462 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
3464 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3465 ( $idr_mayrenew, $idr_error ) =
3466 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3467 is( $idr_mayrenew, 0, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
3468 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 2 rules matched (withdrawn,itype)' );
3469 ( $idr_mayrenew, $idr_error ) =
3470 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3471 is( $idr_mayrenew, 1, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3472 is( $idr_error, undef, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3474 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
3476 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3477 ( $idr_mayrenew, $idr_error ) =
3478 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3479 is( $idr_mayrenew, 0, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
3480 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 3 rules matched (withdrawn,itype, location)' );
3481 ( $idr_mayrenew, $idr_error ) =
3482 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3483 is( $idr_mayrenew, 1, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3484 is( $idr_error, undef, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3486 $idr_rules="itemcallnumber: [NULL]";
3487 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3488 ( $idr_mayrenew, $idr_error ) =
3489 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3490 is( $idr_mayrenew, 0, 'Renewal blocked for undef when NULL in pref' );
3491 $idr_rules="itemcallnumber: ['']";
3492 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3493 ( $idr_mayrenew, $idr_error ) =
3494 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3495 is( $idr_mayrenew, 1, 'Renewal not blocked for undef when "" in pref' );
3497 $idr_rules="itemnotes: [NULL]";
3498 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3499 ( $idr_mayrenew, $idr_error ) =
3500 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3501 is( $idr_mayrenew, 1, 'Renewal not blocked for "" when NULL in pref' );
3502 $idr_rules="itemnotes: ['']";
3503 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3504 ( $idr_mayrenew, $idr_error ) =
3505 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3506 is( $idr_mayrenew, 0, 'Renewal blocked for empty string when "" in pref' );
3509 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
3512 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3513 my $library = $builder->build( { source => 'Branch' } );
3514 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3516 my $item = $builder->build_sample_item(
3518 library => $library->{branchcode},
3522 my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3523 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3524 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3527 subtest 'CanBookBeIssued | notforloan' => sub {
3530 t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
3532 my $library = $builder->build( { source => 'Branch' } );
3533 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3535 my $itemtype = $builder->build(
3537 source => 'Itemtype',
3538 value => { notforloan => undef, }
3541 my $item = $builder->build_sample_item(
3543 library => $library->{branchcode},
3544 itype => $itemtype->{itemtype},
3547 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3549 my ( $issuingimpossible, $needsconfirmation );
3552 subtest 'item-level_itypes = 1' => sub {
3555 t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
3556 # Is for loan at item type and item level
3557 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3558 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3559 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3561 # not for loan at item type level
3562 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3563 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3564 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3567 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3568 'Item can not be issued, not for loan at item type level'
3571 # not for loan at item level
3572 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3573 $item->notforloan( 1 )->store;
3574 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3575 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3578 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3579 'Item can not be issued, not for loan at item type level'
3583 subtest 'item-level_itypes = 0' => sub {
3586 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3588 # We set another itemtype for biblioitem
3589 my $itemtype = $builder->build(
3591 source => 'Itemtype',
3592 value => { notforloan => undef, }
3596 # for loan at item type and item level
3597 $item->notforloan(0)->store;
3598 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3599 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3600 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3601 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3603 # not for loan at item type level
3604 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3605 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3606 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3609 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3610 'Item can not be issued, not for loan at item type level'
3613 # not for loan at item level
3614 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3615 $item->notforloan( 1 )->store;
3616 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3617 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3620 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3621 'Item can not be issued, not for loan at item type level'
3625 # TODO test with AllowNotForLoanOverride = 1
3628 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
3631 t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
3632 my $item = $builder->build_sample_item(
3634 onloan => '2018-01-01',
3638 AddReturn( $item->barcode, $item->homebranch );
3639 $item->discard_changes; # refresh
3640 is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
3644 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
3649 t::lib::Mocks::mock_preference('item-level_itypes', 1);
3651 my $issuing_charges = 15;
3652 my $title = 'A title';
3653 my $author = 'Author, An';
3654 my $barcode = 'WHATARETHEODDS';
3656 my $circ = Test::MockModule->new('C4::Circulation');
3658 'GetIssuingCharges',
3660 return $issuing_charges;
3664 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3665 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
3666 my $patron = $builder->build_object({
3667 class => 'Koha::Patrons',
3668 value => { branchcode => $library->id }
3671 my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
3672 my $item_id = Koha::Item->new(
3674 biblionumber => $biblio->biblionumber,
3675 homebranch => $library->id,
3676 holdingbranch => $library->id,
3677 barcode => $barcode,
3678 replacementprice => 23.00,
3679 itype => $itemtype->id
3681 )->store->itemnumber;
3682 my $item = Koha::Items->find( $item_id );
3684 my $context = Test::MockModule->new('C4::Context');
3685 $context->mock( userenv => { branch => $library->id } );
3687 # Check the item out
3688 AddIssue( $patron->unblessed, $item->barcode );
3689 t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
3690 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
3691 my %params_renewal = (
3692 timestamp => { -like => $date . "%" },
3693 module => "CIRCULATION",
3694 action => "RENEWAL",
3696 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
3697 AddRenewal( $patron->id, $item->id, $library->id );
3698 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3699 is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
3701 my $checkouts = $patron->checkouts;
3702 # The following will fail if run on 00:00:00
3703 unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
3705 my $lines = Koha::Account::Lines->search({
3706 borrowernumber => $patron->id,
3707 itemnumber => $item->id
3710 is( $lines->count, 2 );
3712 my $line = $lines->next;
3713 is( $line->debit_type_code, 'RENT', 'The issue of item with issuing charge generates an accountline of the correct type' );
3714 is( $line->branchcode, $library->id, 'AddIssuingCharge correctly sets branchcode' );
3715 is( $line->description, '', 'AddIssue does not set a hardcoded description for the accountline' );
3717 $line = $lines->next;
3718 is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
3719 is( $line->branchcode, $library->id, 'AddRenewal correctly sets branchcode' );
3720 is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
3722 t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
3724 $context = Test::MockModule->new('C4::Context');
3725 $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
3727 my $now = dt_from_string;
3728 $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
3729 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
3730 my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
3731 $sth->execute($item->id, $library->id);
3732 my ($old_stats_size) = $sth->fetchrow_array;
3733 AddRenewal( $patron->id, $item->id, $library->id );
3734 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3735 $sth->execute($item->id, $library->id);
3736 my ($new_stats_size) = $sth->fetchrow_array;
3737 is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
3738 is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
3740 AddReturn( $item->id, $library->id, undef, $date );
3741 AddIssue( $patron->unblessed, $item->barcode, $now );
3742 AddRenewal( $patron->id, $item->id, $library->id, undef, undef, 1 );
3743 my $lines_skipped = Koha::Account::Lines->search({
3744 borrowernumber => $patron->id,
3745 itemnumber => $item->id
3747 is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
3751 subtest 'ProcessOfflinePayment() tests' => sub {
3758 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
3759 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3760 my $result = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
3762 is( $result, 'Success.', 'The right string is returned' );
3764 my $lines = $patron->account->lines;
3765 is( $lines->count, 1, 'line created correctly');
3767 my $line = $lines->next;
3768 is( $line->amount+0, $amount * -1, 'amount picked from params' );
3769 is( $line->branchcode, $library->id, 'branchcode set correctly' );
3773 subtest 'Incremented fee tests' => sub {
3776 my $dt = dt_from_string();
3777 Time::Fake->offset( $dt->epoch );
3779 t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
3782 $builder->build_object( { class => 'Koha::Libraries' } )->store;
3784 my $module = new Test::MockModule('C4::Context');
3785 $module->mock( 'userenv', sub { { branch => $library->id } } );
3787 my $patron = $builder->build_object(
3789 class => 'Koha::Patrons',
3790 value => { categorycode => $patron_category->{categorycode} }
3794 my $itemtype = $builder->build_object(
3796 class => 'Koha::ItemTypes',
3798 notforloan => undef,
3800 rentalcharge_daily => 1,
3801 rentalcharge_daily_calendar => 0
3806 my $item = $builder->build_sample_item(
3808 library => $library->{branchcode},
3809 itype => $itemtype->id,
3813 is( $itemtype->rentalcharge_daily+0,
3814 1, 'Daily rental charge stored and retreived correctly' );
3815 is( $item->effective_itemtype, $itemtype->id,
3816 "Itemtype set correctly for item" );
3818 my $now = dt_from_string;
3819 my $dt_from = $now->clone;
3820 my $dt_to = $now->clone->add( days => 7 );
3821 my $dt_to_renew = $now->clone->add( days => 13 );
3825 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3826 my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3827 is( $accountline->amount+0, 7,
3828 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
3830 $accountline->delete();
3831 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3832 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3833 is( $accountline->amount+0, 6,
3834 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
3836 $accountline->delete();
3839 t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
3840 $itemtype->rentalcharge_daily_calendar(1)->store();
3842 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3843 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3844 is( $accountline->amount+0, 7,
3845 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
3847 $accountline->delete();
3848 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3849 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3850 is( $accountline->amount+0, 6,
3851 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
3853 $accountline->delete();
3856 my $calendar = C4::Calendar->new( branchcode => $library->id );
3857 # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
3859 ( $dt_from->day_of_week == 6 ) ? 0
3860 : ( $dt_from->day_of_week == 7 ) ? 1
3861 : $dt_from->day_of_week + 1;
3862 my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
3863 $calendar->insert_week_day_holiday(
3864 weekday => $closed_day,
3865 title => 'Test holiday',
3866 description => 'Test holiday'
3869 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3870 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3871 is( $accountline->amount+0, 6,
3872 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
3874 $accountline->delete();
3875 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3876 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3877 is( $accountline->amount+0, 5,
3878 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
3880 $accountline->delete();
3883 $itemtype->rentalcharge(2)->store;
3884 is( $itemtype->rentalcharge+0, 2,
3885 'Rental charge updated and retreived correctly' );
3887 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3889 Koha::Account::Lines->search( { itemnumber => $item->id } );
3890 is( $accountlines->count, '2',
3891 "Fixed charge and accrued charge recorded distinctly" );
3892 $accountlines->delete();
3893 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3894 $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
3895 is( $accountlines->count, '2',
3896 "Fixed charge and accrued charge recorded distinctly, for renewal" );
3897 $accountlines->delete();
3899 $itemtype->rentalcharge(0)->store;
3900 is( $itemtype->rentalcharge+0, 0,
3901 'Rental charge reset and retreived correctly' );
3904 Koha::CirculationRules->set_rule(
3906 categorycode => $patron->categorycode,
3907 itemtype => $itemtype->id,
3908 branchcode => $library->id,
3909 rule_name => 'lengthunit',
3910 rule_value => 'hours',
3914 $itemtype->rentalcharge_hourly('0.25')->store();
3915 is( $itemtype->rentalcharge_hourly,
3916 '0.25', 'Hourly rental charge stored and retreived correctly' );
3918 $dt_to = $now->clone->add( hours => 168 );
3919 $dt_to_renew = $now->clone->add( hours => 312 );
3921 $itemtype->rentalcharge_hourly_calendar(0)->store();
3923 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3924 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3925 is( $accountline->amount + 0, 42,
3926 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)" );
3927 $accountline->delete();
3928 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3929 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3930 is( $accountline->amount + 0, 36,
3931 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)" );
3932 $accountline->delete();
3935 $itemtype->rentalcharge_hourly_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, 36,
3940 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)" );
3941 $accountline->delete();
3942 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3943 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3944 is( $accountline->amount + 0, 30,
3945 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u" );
3946 $accountline->delete();
3949 $calendar->delete_holiday( weekday => $closed_day );
3951 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3952 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3953 is( $accountline->amount + 0, 42,
3954 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u" );
3955 $accountline->delete();
3956 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3957 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3958 is( $accountline->amount + 0, 36,
3959 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)" );
3960 $accountline->delete();
3965 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
3968 t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
3969 t::lib::Mocks::mock_preference('item-level_itypes', 1);
3972 $builder->build_object( { class => 'Koha::Libraries' } )->store;
3973 my $patron = $builder->build_object(
3975 class => 'Koha::Patrons',
3976 value => { categorycode => $patron_category->{categorycode} }
3980 my $itemtype = $builder->build_object(
3982 class => 'Koha::ItemTypes',
3986 rentalcharge_daily => 0
3991 my $biblioitem = $builder->build( { source => 'Biblioitem' } );
3992 my $item = $builder->build_object(
3994 class => 'Koha::Items',
3996 homebranch => $library->id,
3997 holdingbranch => $library->id,
4001 itype => $itemtype->id,
4002 biblionumber => $biblioitem->{biblionumber},
4003 biblioitemnumber => $biblioitem->{biblioitemnumber},
4008 my ( $issuingimpossible, $needsconfirmation );
4009 my $dt_from = dt_from_string();
4010 my $dt_due = $dt_from->clone->add( days => 3 );
4012 $itemtype->rentalcharge(1)->store;
4013 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4014 is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
4015 $itemtype->rentalcharge('0')->store;
4016 $itemtype->rentalcharge_daily(1)->store;
4017 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4018 is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
4019 $itemtype->rentalcharge_daily('0')->store;
4022 subtest 'Do not return on renewal (LOST charge)' => sub {
4025 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
4026 my $library = $builder->build_object( { class => "Koha::Libraries" } );
4027 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
4028 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
4030 my $biblio = $builder->build_sample_biblio;
4032 my $item = $builder->build_sample_item(
4034 biblionumber => $biblio->biblionumber,
4035 library => $library->branchcode,
4036 replacementprice => 99.00,
4041 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
4042 AddIssue( $patron->unblessed, $item->barcode );
4044 my $accountline = Koha::Account::Line->new(
4046 borrowernumber => $patron->borrowernumber,
4047 debit_type_code => 'LOST',
4049 itemnumber => $item->itemnumber,
4051 amountoutstanding => 12,
4052 interface => 'something',
4056 # AddRenewal doesn't call _FixAccountForLostAndFound
4057 AddIssue( $patron->unblessed, $item->barcode );
4059 is( $patron->checkouts->count, 1,
4060 'Renewal should not return the item even if a LOST payment has been made earlier'
4064 subtest 'Filling a hold should cancel existing transfer' => sub {
4067 t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
4069 my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
4070 my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
4071 my $patron = $builder->build_object(
4073 class => 'Koha::Patrons',
4075 categorycode => $patron_category->{categorycode},
4076 branchcode => $libraryA->branchcode,
4081 my $item = $builder->build_sample_item({
4082 homebranch => $libraryB->branchcode,
4085 my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4086 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
4088 branchcode => $libraryA->branchcode,
4089 borrowernumber => $patron->borrowernumber,
4090 biblionumber => $item->biblionumber,
4091 itemnumber => $item->itemnumber
4093 my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
4094 is( $reserves->count, 1, "Reserve is placed");
4095 ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4096 my $reserve = $reserves->next;
4097 ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
4098 $reserve->discard_changes;
4099 ok( $reserve->found eq 'W', "Reserve is marked waiting" );
4100 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
4103 $schema->storage->txn_rollback;
4104 C4::Context->clear_syspref_cache();
4105 $branches = Koha::Libraries->search();
4106 for my $branch ( $branches->next ) {
4107 my $key = $branch->branchcode . "_holidays";
4108 $cache->clear_from_cache($key);