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 => 50;
24 use Test::Deep qw( cmp_deeply );
30 use POSIX qw( floor );
32 use t::lib::TestBuilder;
41 use C4::Overdues qw(UpdateFine CalcFine);
45 use Koha::Item::Transfers;
49 use Koha::CirculationRules;
50 use Koha::Subscriptions;
51 use Koha::Account::Lines;
52 use Koha::Account::Offsets;
57 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
61 my ( $error, $question, $alert ) = @_;
63 $s = %$error ? ' (error: ' . join( ' ', keys %$error ) . ')' : '';
64 $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
65 $s .= %$alert ? ' (alert: ' . join( ' ', keys %$alert ) . ')' : '';
69 sub test_debarment_on_checkout {
71 my $item = $params->{item};
72 my $library = $params->{library};
73 my $patron = $params->{patron};
74 my $due_date = $params->{due_date} || dt_from_string;
75 my $return_date = $params->{return_date} || dt_from_string;
76 my $expected_expiration_date = $params->{expiration_date};
78 $expected_expiration_date = output_pref(
80 dt => $expected_expiration_date,
86 my $line_number = $caller[2];
87 AddIssue( $patron, $item->barcode, $due_date );
89 my ( undef, $message ) = AddReturn( $item->barcode, $library->{branchcode}, undef, $return_date );
90 is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
91 or diag('AddReturn returned message ' . Dumper $message );
92 my $debarments = Koha::Patron::Debarments::GetDebarments(
93 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
94 is( scalar(@$debarments), 1, 'Test at line ' . $line_number );
96 is( $debarments->[0]->{expiration},
97 $expected_expiration_date, 'Test at line ' . $line_number );
98 Koha::Patron::Debarments::DelUniqueDebarment(
99 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
102 my $schema = Koha::Database->schema;
103 $schema->storage->txn_begin;
104 my $builder = t::lib::TestBuilder->new;
105 my $dbh = C4::Context->dbh;
107 # Prevent random failures by mocking ->now
108 my $now_value = dt_from_string;
109 my $mocked_datetime = Test::MockModule->new('DateTime');
110 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
112 my $cache = Koha::Caches->get_instance();
113 $dbh->do(q|DELETE FROM special_holidays|);
114 $dbh->do(q|DELETE FROM repeatable_holidays|);
115 my $branches = Koha::Libraries->search();
116 for my $branch ( $branches->next ) {
117 my $key = $branch->branchcode . "_holidays";
118 $cache->clear_from_cache($key);
121 # Start with a clean slate
122 $dbh->do('DELETE FROM issues');
123 $dbh->do('DELETE FROM borrowers');
125 my $library = $builder->build({
128 my $library2 = $builder->build({
131 my $itemtype = $builder->build(
133 source => 'Itemtype',
137 rentalcharge_daily => 0,
138 defaultreplacecost => undef,
143 my $patron_category = $builder->build(
145 source => 'Category',
147 category_type => 'P',
149 BlockExpiredPatronOpacActions => -1, # Pick the pref value
154 my $CircControl = C4::Context->preference('CircControl');
155 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
158 homebranch => $library2->{branchcode},
159 holdingbranch => $library2->{branchcode}
163 branchcode => $library2->{branchcode}
166 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
168 # No userenv, PickupLibrary
169 t::lib::Mocks::mock_preference('IndependentBranches', '0');
170 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
172 C4::Context->preference('CircControl'),
174 'CircControl changed to PickupLibrary'
177 C4::Circulation::_GetCircControlBranch($item, $borrower),
178 $item->{$HomeOrHoldingBranch},
179 '_GetCircControlBranch returned item branch (no userenv defined)'
182 # No userenv, PatronLibrary
183 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
185 C4::Context->preference('CircControl'),
187 'CircControl changed to PatronLibrary'
190 C4::Circulation::_GetCircControlBranch($item, $borrower),
191 $borrower->{branchcode},
192 '_GetCircControlBranch returned borrower branch'
195 # No userenv, ItemHomeLibrary
196 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
198 C4::Context->preference('CircControl'),
200 'CircControl changed to ItemHomeLibrary'
203 $item->{$HomeOrHoldingBranch},
204 C4::Circulation::_GetCircControlBranch($item, $borrower),
205 '_GetCircControlBranch returned item branch'
209 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
210 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
212 # Userenv set, PickupLibrary
213 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
215 C4::Context->preference('CircControl'),
217 'CircControl changed to PickupLibrary'
220 C4::Circulation::_GetCircControlBranch($item, $borrower),
221 $library2->{branchcode},
222 '_GetCircControlBranch returned current branch'
225 # Userenv set, PatronLibrary
226 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
228 C4::Context->preference('CircControl'),
230 'CircControl changed to PatronLibrary'
233 C4::Circulation::_GetCircControlBranch($item, $borrower),
234 $borrower->{branchcode},
235 '_GetCircControlBranch returned borrower branch'
238 # Userenv set, ItemHomeLibrary
239 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
241 C4::Context->preference('CircControl'),
243 'CircControl changed to ItemHomeLibrary'
246 C4::Circulation::_GetCircControlBranch($item, $borrower),
247 $item->{$HomeOrHoldingBranch},
248 '_GetCircControlBranch returned item branch'
251 # Reset initial configuration
252 t::lib::Mocks::mock_preference('CircControl', $CircControl);
254 C4::Context->preference('CircControl'),
256 'CircControl reset to its initial value'
259 # Set a simple circ policy
260 $dbh->do('DELETE FROM circulation_rules');
261 Koha::CirculationRules->set_rules(
263 categorycode => undef,
267 reservesallowed => 25,
269 lengthunit => 'days',
270 renewalsallowed => 1,
272 norenewalbefore => undef,
280 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
281 subtest "CanBookBeRenewed tests" => sub {
284 C4::Context->set_preference('ItemsDeniedRenewal','');
285 # Generate test biblio
286 my $biblio = $builder->build_sample_biblio();
288 my $branch = $library2->{branchcode};
290 my $item_1 = $builder->build_sample_item(
292 biblionumber => $biblio->biblionumber,
294 replacementprice => 12.00,
298 $reused_itemnumber_1 = $item_1->itemnumber;
300 my $item_2 = $builder->build_sample_item(
302 biblionumber => $biblio->biblionumber,
304 replacementprice => 23.00,
308 $reused_itemnumber_2 = $item_2->itemnumber;
310 my $item_3 = $builder->build_sample_item(
312 biblionumber => $biblio->biblionumber,
314 replacementprice => 23.00,
320 my %renewing_borrower_data = (
322 surname => 'Renewal',
323 categorycode => $patron_category->{categorycode},
324 branchcode => $branch,
327 my %reserving_borrower_data = (
328 firstname => 'Katrin',
329 surname => 'Reservation',
330 categorycode => $patron_category->{categorycode},
331 branchcode => $branch,
334 my %hold_waiting_borrower_data = (
336 surname => 'Reservation',
337 categorycode => $patron_category->{categorycode},
338 branchcode => $branch,
341 my %restricted_borrower_data = (
342 firstname => 'Alice',
343 surname => 'Reservation',
344 categorycode => $patron_category->{categorycode},
345 debarred => '3228-01-01',
346 branchcode => $branch,
349 my %expired_borrower_data = (
352 categorycode => $patron_category->{categorycode},
353 branchcode => $branch,
354 dateexpiry => dt_from_string->subtract( months => 1 ),
357 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
358 my $reserving_borrowernumber = Koha::Patron->new(\%reserving_borrower_data)->store->borrowernumber;
359 my $hold_waiting_borrowernumber = Koha::Patron->new(\%hold_waiting_borrower_data)->store->borrowernumber;
360 my $restricted_borrowernumber = Koha::Patron->new(\%restricted_borrower_data)->store->borrowernumber;
361 my $expired_borrowernumber = Koha::Patron->new(\%expired_borrower_data)->store->borrowernumber;
363 my $renewing_borrower_obj = Koha::Patrons->find( $renewing_borrowernumber );
364 my $renewing_borrower = $renewing_borrower_obj->unblessed;
365 my $restricted_borrower = Koha::Patrons->find( $restricted_borrowernumber )->unblessed;
366 my $expired_borrower = Koha::Patrons->find( $expired_borrowernumber )->unblessed;
373 my $checkitem = undef;
376 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
377 my $datedue = dt_from_string( $issue->date_due() );
378 is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
380 my $issue2 = AddIssue( $renewing_borrower, $item_2->barcode);
381 $datedue = dt_from_string( $issue->date_due() );
382 is (defined $issue2, 1, "Item 2 checked out, due date: " . $issue2->date_due());
385 my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
386 is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}");
388 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
389 is( $renewokay, 1, 'Can renew, no holds for this title or item');
392 # Biblio-level hold, renewal test
395 branchcode => $branch,
396 borrowernumber => $reserving_borrowernumber,
397 biblionumber => $biblio->biblionumber,
398 priority => $priority,
399 reservation_date => $resdate,
400 expiration_date => $expdate,
402 itemnumber => $checkitem,
407 # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
408 Koha::CirculationRules->set_rule(
410 categorycode => undef,
413 rule_name => 'onshelfholds',
417 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
418 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
419 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
420 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
421 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
423 # Now let's add an item level hold, we should no longer be able to renew the item
424 my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
426 borrowernumber => $hold_waiting_borrowernumber,
427 biblionumber => $biblio->biblionumber,
428 itemnumber => $item_1->itemnumber,
429 branchcode => $branch,
433 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
434 is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
437 # 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
438 # be able to renew these items
439 $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
441 borrowernumber => $hold_waiting_borrowernumber,
442 biblionumber => $biblio->biblionumber,
443 itemnumber => $item_3->itemnumber,
444 branchcode => $branch,
449 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
450 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
451 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
452 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
453 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
455 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
456 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
457 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
459 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
460 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
461 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
463 my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
464 my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber )->unblessed;
465 AddIssue($reserving_borrower, $item_3->barcode);
466 my $reserve = $dbh->selectrow_hashref(
467 'SELECT * FROM old_reserves WHERE reserve_id = ?',
471 is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
473 # Item-level hold, renewal test
476 branchcode => $branch,
477 borrowernumber => $reserving_borrowernumber,
478 biblionumber => $biblio->biblionumber,
479 priority => $priority,
480 reservation_date => $resdate,
481 expiration_date => $expdate,
483 itemnumber => $item_1->itemnumber,
488 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
489 is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
490 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
492 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber, 1);
493 is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
495 # Items can't fill hold for reasons
496 $item_1->notforloan(1)->store;
497 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
498 is( $renewokay, 1, 'Can renew, item is marked not for loan, hold does not block');
499 $item_1->set({notforloan => 0, itype => $itemtype })->store;
501 # FIXME: Add more for itemtype not for loan etc.
503 # Restricted users cannot renew when RestrictionBlockRenewing is enabled
504 my $item_5 = $builder->build_sample_item(
506 biblionumber => $biblio->biblionumber,
508 replacementprice => 23.00,
512 my $datedue5 = AddIssue($restricted_borrower, $item_5->barcode);
513 is (defined $datedue5, 1, "Item with date due checked out, due date: $datedue5");
515 t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
516 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
517 is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
518 ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $item_5->itemnumber);
519 is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
521 # Users cannot renew an overdue item
522 my $item_6 = $builder->build_sample_item(
524 biblionumber => $biblio->biblionumber,
526 replacementprice => 23.00,
531 my $item_7 = $builder->build_sample_item(
533 biblionumber => $biblio->biblionumber,
535 replacementprice => 23.00,
540 my $datedue6 = AddIssue( $renewing_borrower, $item_6->barcode);
541 is (defined $datedue6, 1, "Item 2 checked out, due date: ".$datedue6->date_due);
543 my $now = dt_from_string();
544 my $five_weeks = DateTime::Duration->new(weeks => 5);
545 my $five_weeks_ago = $now - $five_weeks;
546 t::lib::Mocks::mock_preference('finesMode', 'production');
548 my $passeddatedue1 = AddIssue($renewing_borrower, $item_7->barcode, $five_weeks_ago);
549 is (defined $passeddatedue1, 1, "Item with passed date due checked out, due date: " . $passeddatedue1->date_due);
551 my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower->{categorycode}, $branch, $five_weeks_ago, $now );
552 C4::Overdues::UpdateFine(
554 issue_id => $passeddatedue1->id(),
555 itemnumber => $item_7->itemnumber,
556 borrowernumber => $renewing_borrower->{borrowernumber},
558 due => Koha::DateUtils::output_pref($five_weeks_ago)
562 t::lib::Mocks::mock_preference('RenewalLog', 0);
563 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
564 my %params_renewal = (
565 timestamp => { -like => $date . "%" },
566 module => "CIRCULATION",
570 timestamp => { -like => $date . "%" },
571 module => "CIRCULATION",
574 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );
575 my $dt = dt_from_string();
576 Time::Fake->offset( $dt->epoch );
577 my $datedue1 = AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
578 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
579 is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
580 isnt (DateTime->compare($datedue1, $dt), 0, "AddRenewal returned a good duedate");
583 t::lib::Mocks::mock_preference('RenewalLog', 1);
584 $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
585 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
586 AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
587 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
588 is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
590 my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
591 is( $fines->count, 2, 'AddRenewal left both fines' );
592 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
593 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
597 my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
598 my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
599 AddIssue( $renewing_borrower,$item_7->barcode,Koha::DateUtils::output_pref({str=>$datedue6->date_due, dateformat =>'iso'}),0,$date, 0, undef );
600 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
601 is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
602 $new_log_size = Koha::ActionLogs->count( \%params_issue );
603 is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
605 $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
608 t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
609 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
610 is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
611 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
612 is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
615 $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
619 # Test automatic renewal before value for "norenewalbefore" in policy is set
620 # In this case automatic renewal is not permitted prior to due date
621 my $item_4 = $builder->build_sample_item(
623 biblionumber => $biblio->biblionumber,
625 replacementprice => 16.00,
630 $issue = AddIssue( $renewing_borrower, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
631 ( $renewokay, $error ) =
632 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
633 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
634 is( $error, 'auto_too_soon',
635 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
638 branchcode => $branch,
639 borrowernumber => $reserving_borrowernumber,
640 biblionumber => $biblio->biblionumber,
641 itemnumber => $bibitems,
642 priority => $priority,
643 reservation_date => $resdate,
644 expiration_date => $expdate,
647 itemnumber => $item_4->itemnumber,
651 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
652 is( $renewokay, 0, 'Still should not be able to renew' );
653 is( $error, 'on_reserve', 'returned code is on_reserve, reserve checked when not checking for cron' );
654 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, undef, 1 );
655 is( $renewokay, 0, 'Still should not be able to renew' );
656 is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked when checking for cron' );
657 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
658 is( $renewokay, 0, 'Still should not be able to renew' );
659 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
660 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1, 1 );
661 is( $renewokay, 0, 'Still should not be able to renew' );
662 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
663 $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
664 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
665 is( $renewokay, 0, 'Still should not be able to renew' );
666 is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
667 ModReserveCancelAll($item_4->itemnumber, $reserving_borrowernumber);
671 $renewing_borrower_obj->autorenew_checkouts(0)->store;
672 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
673 is( $renewokay, 1, 'No renewal before is undef, but patron opted out of auto_renewal' );
674 $renewing_borrower_obj->autorenew_checkouts(1)->store;
678 # Test premature manual renewal
679 Koha::CirculationRules->set_rule(
681 categorycode => undef,
684 rule_name => 'norenewalbefore',
689 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
690 is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
691 is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
694 # Test 'exact time' setting for syspref NoRenewalBeforePrecision
695 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
697 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
698 $datedue->clone->add( days => -7 ),
699 'Bug 14395: Renewals permitted 7 days before due date, as expected'
703 # Test 'date' setting for syspref NoRenewalBeforePrecision
704 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
706 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
707 $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
708 'Bug 14395: Renewals permitted 7 days before due date, as expected'
712 # Test premature automatic renewal
713 ( $renewokay, $error ) =
714 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
715 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
716 is( $error, 'auto_too_soon',
717 'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
720 $renewing_borrower_obj->autorenew_checkouts(0)->store;
721 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
722 is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
723 is( $error, 'too_soon', 'Error is too_soon, no auto' );
724 $renewing_borrower_obj->autorenew_checkouts(1)->store;
726 # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
727 # and test automatic renewal again
728 $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
729 ( $renewokay, $error ) =
730 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
731 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
732 is( $error, 'auto_too_soon',
733 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
736 $renewing_borrower_obj->autorenew_checkouts(0)->store;
737 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
738 is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
739 is( $error, 'too_soon', 'Error is too_soon, no auto' );
740 $renewing_borrower_obj->autorenew_checkouts(1)->store;
742 # Change policy so that loans can be renewed 99 days prior to the due date
743 # and test automatic renewal again
744 $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
745 ( $renewokay, $error ) =
746 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
747 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
748 is( $error, 'auto_renew',
749 'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
752 $renewing_borrower_obj->autorenew_checkouts(0)->store;
753 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
754 is( $renewokay, 1, 'No renewal before is 99, patron opted out of auto_renewal so can renew' );
755 $renewing_borrower_obj->autorenew_checkouts(1)->store;
757 subtest "too_late_renewal / no_auto_renewal_after" => sub {
759 my $item_to_auto_renew = $builder->build_sample_item(
761 biblionumber => $biblio->biblionumber,
766 my $ten_days_before = dt_from_string->add( days => -10 );
767 my $ten_days_ahead = dt_from_string->add( days => 10 );
768 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
770 Koha::CirculationRules->set_rules(
772 categorycode => undef,
776 norenewalbefore => '7',
777 no_auto_renewal_after => '9',
781 ( $renewokay, $error ) =
782 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
783 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
784 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
786 Koha::CirculationRules->set_rules(
788 categorycode => undef,
792 norenewalbefore => '7',
793 no_auto_renewal_after => '10',
797 ( $renewokay, $error ) =
798 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
799 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
800 is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
802 Koha::CirculationRules->set_rules(
804 categorycode => undef,
808 norenewalbefore => '7',
809 no_auto_renewal_after => '11',
813 ( $renewokay, $error ) =
814 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
815 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
816 is( $error, 'auto_too_soon', 'Cannot auto renew, too soon - no_auto_renewal_after is defined(returned code is auto_too_soon)' );
818 Koha::CirculationRules->set_rules(
820 categorycode => undef,
824 norenewalbefore => '10',
825 no_auto_renewal_after => '11',
829 ( $renewokay, $error ) =
830 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
831 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
832 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
834 Koha::CirculationRules->set_rules(
836 categorycode => undef,
840 norenewalbefore => '10',
841 no_auto_renewal_after => undef,
842 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
846 ( $renewokay, $error ) =
847 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
848 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
849 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
851 Koha::CirculationRules->set_rules(
853 categorycode => undef,
857 norenewalbefore => '7',
858 no_auto_renewal_after => '15',
859 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
863 ( $renewokay, $error ) =
864 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
865 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
866 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
868 Koha::CirculationRules->set_rules(
870 categorycode => undef,
874 norenewalbefore => '10',
875 no_auto_renewal_after => undef,
876 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 1 ),
880 ( $renewokay, $error ) =
881 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
882 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
883 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
886 subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew & OPACFineNoRenewalsIncludeCredit" => sub {
888 my $item_to_auto_renew = $builder->build_sample_item(
890 biblionumber => $biblio->biblionumber,
895 my $ten_days_before = dt_from_string->add( days => -10 );
896 my $ten_days_ahead = dt_from_string->add( days => 10 );
897 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
899 Koha::CirculationRules->set_rules(
901 categorycode => undef,
905 norenewalbefore => '10',
906 no_auto_renewal_after => '11',
910 C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
911 C4::Context->set_preference('OPACFineNoRenewals','10');
912 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
913 my $fines_amount = 5;
914 my $account = Koha::Account->new({patron_id => $renewing_borrowernumber});
917 amount => $fines_amount,
920 item_id => $item_to_auto_renew->itemnumber,
921 description => "Some fines"
923 )->status('RETURNED')->store;
924 ( $renewokay, $error ) =
925 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
926 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
927 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
931 amount => $fines_amount,
934 item_id => $item_to_auto_renew->itemnumber,
935 description => "Some fines"
937 )->status('RETURNED')->store;
938 ( $renewokay, $error ) =
939 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
940 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
941 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
945 amount => $fines_amount,
948 item_id => $item_to_auto_renew->itemnumber,
949 description => "Some fines"
951 )->status('RETURNED')->store;
952 ( $renewokay, $error ) =
953 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
954 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
955 is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
957 $account->add_credit(
959 amount => $fines_amount,
962 description => "Some payment"
965 ( $renewokay, $error ) =
966 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
967 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
968 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
970 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','0');
971 ( $renewokay, $error ) =
972 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
973 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
974 is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
976 $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
977 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
980 subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
982 my $item_to_auto_renew = $builder->build_sample_item(
984 biblionumber => $biblio->biblionumber,
989 Koha::CirculationRules->set_rules(
991 categorycode => undef,
995 norenewalbefore => 10,
996 no_auto_renewal_after => 11,
1001 my $ten_days_before = dt_from_string->add( days => -10 );
1002 my $ten_days_ahead = dt_from_string->add( days => 10 );
1004 # Patron is expired and BlockExpiredPatronOpacActions=0
1005 # => auto renew is allowed
1006 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
1007 my $patron = $expired_borrower;
1008 my $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1009 ( $renewokay, $error ) =
1010 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1011 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1012 is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
1013 Koha::Checkouts->find( $checkout->issue_id )->delete;
1016 # Patron is expired and BlockExpiredPatronOpacActions=1
1017 # => auto renew is not allowed
1018 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1019 $patron = $expired_borrower;
1020 $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1021 ( $renewokay, $error ) =
1022 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1023 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1024 is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
1025 Koha::Checkouts->find( $checkout->issue_id )->delete;
1028 # Patron is not expired and BlockExpiredPatronOpacActions=1
1029 # => auto renew is allowed
1030 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1031 $patron = $renewing_borrower;
1032 $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1033 ( $renewokay, $error ) =
1034 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1035 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1036 is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
1037 Koha::Checkouts->find( $checkout->issue_id )->delete;
1040 subtest "GetLatestAutoRenewDate" => sub {
1042 my $item_to_auto_renew = $builder->build_sample_item(
1044 biblionumber => $biblio->biblionumber,
1049 my $ten_days_before = dt_from_string->add( days => -10 );
1050 my $ten_days_ahead = dt_from_string->add( days => 10 );
1051 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1052 Koha::CirculationRules->set_rules(
1054 categorycode => undef,
1055 branchcode => undef,
1058 norenewalbefore => '7',
1059 no_auto_renewal_after => '',
1060 no_auto_renewal_after_hard_limit => undef,
1064 my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1065 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' );
1066 my $five_days_before = dt_from_string->add( days => -5 );
1067 Koha::CirculationRules->set_rules(
1069 categorycode => undef,
1070 branchcode => undef,
1073 norenewalbefore => '10',
1074 no_auto_renewal_after => '5',
1075 no_auto_renewal_after_hard_limit => undef,
1079 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1080 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1081 $five_days_before->truncate( to => 'minute' ),
1082 'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
1084 my $five_days_ahead = dt_from_string->add( days => 5 );
1085 $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
1086 $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
1087 $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
1088 Koha::CirculationRules->set_rules(
1090 categorycode => undef,
1091 branchcode => undef,
1094 norenewalbefore => '10',
1095 no_auto_renewal_after => '15',
1096 no_auto_renewal_after_hard_limit => undef,
1100 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1101 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1102 $five_days_ahead->truncate( to => 'minute' ),
1103 'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
1105 my $two_days_ahead = dt_from_string->add( days => 2 );
1106 Koha::CirculationRules->set_rules(
1108 categorycode => undef,
1109 branchcode => undef,
1112 norenewalbefore => '10',
1113 no_auto_renewal_after => '',
1114 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1118 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1119 is( $latest_auto_renew_date->truncate( to => 'day' ),
1120 $two_days_ahead->truncate( to => 'day' ),
1121 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
1123 Koha::CirculationRules->set_rules(
1125 categorycode => undef,
1126 branchcode => undef,
1129 norenewalbefore => '10',
1130 no_auto_renewal_after => '15',
1131 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1135 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1136 is( $latest_auto_renew_date->truncate( to => 'day' ),
1137 $two_days_ahead->truncate( to => 'day' ),
1138 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
1144 # set policy to forbid renewals
1145 Koha::CirculationRules->set_rules(
1147 categorycode => undef,
1148 branchcode => undef,
1151 norenewalbefore => undef,
1152 renewalsallowed => 0,
1157 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
1158 is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
1159 is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
1161 # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
1162 t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
1163 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1165 C4::Overdues::UpdateFine(
1167 issue_id => $issue->id(),
1168 itemnumber => $item_1->itemnumber,
1169 borrowernumber => $renewing_borrower->{borrowernumber},
1172 due => Koha::DateUtils::output_pref($datedue)
1176 my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower->{borrowernumber} })->next();
1177 is( $line->debit_type_code, 'OVERDUE', 'Account line type is OVERDUE' );
1178 is( $line->status, 'UNRETURNED', 'Account line status is UNRETURNED' );
1179 is( $line->amountoutstanding+0, 15, 'Account line amount outstanding is 15.00' );
1180 is( $line->amount+0, 15, 'Account line amount is 15.00' );
1181 is( $line->issue_id, $issue->id, 'Account line issue id matches' );
1183 my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
1184 is( $offset->type, 'OVERDUE', 'Account offset type is Fine' );
1185 is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
1187 t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
1188 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
1190 LostItem( $item_1->itemnumber, 'test', 1 );
1192 $line = Koha::Account::Lines->find($line->id);
1193 is( $line->debit_type_code, 'OVERDUE', 'Account type remains as OVERDUE' );
1194 isnt( $line->status, 'UNRETURNED', 'Account status correctly changed from UNRETURNED to RETURNED' );
1196 my $item = Koha::Items->find($item_1->itemnumber);
1197 ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
1198 my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
1199 is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
1201 my $total_due = $dbh->selectrow_array(
1202 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1203 undef, $renewing_borrower->{borrowernumber}
1206 is( $total_due+0, 15, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
1208 C4::Context->dbh->do("DELETE FROM accountlines");
1210 C4::Overdues::UpdateFine(
1212 issue_id => $issue2->id(),
1213 itemnumber => $item_2->itemnumber,
1214 borrowernumber => $renewing_borrower->{borrowernumber},
1217 due => Koha::DateUtils::output_pref($datedue)
1221 LostItem( $item_2->itemnumber, 'test', 0 );
1223 my $item2 = Koha::Items->find($item_2->itemnumber);
1224 ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
1225 ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
1227 $total_due = $dbh->selectrow_array(
1228 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1229 undef, $renewing_borrower->{borrowernumber}
1232 ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
1234 my $future = dt_from_string();
1235 $future->add( days => 7 );
1236 my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
1237 ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
1239 # Users cannot renew any item if there is an overdue item
1240 t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
1241 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
1242 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1243 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
1244 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1246 my $manager = $builder->build_object({ class => "Koha::Patrons" });
1247 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
1248 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1249 $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
1250 LostItem( $item_3->itemnumber, 'test', 0 );
1251 my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
1252 is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
1254 $accountline->description,
1255 sprintf( "%s %s %s",
1256 $item_3->biblio->title || '',
1257 $item_3->barcode || '',
1258 $item_3->itemcallnumber || '' ),
1259 "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1263 subtest "GetUpcomingDueIssues" => sub {
1266 my $branch = $library2->{branchcode};
1268 #Create another record
1269 my $biblio2 = $builder->build_sample_biblio();
1272 my $item_1 = Koha::Items->find($reused_itemnumber_1);
1273 my $item_2 = Koha::Items->find($reused_itemnumber_2);
1274 my $item_3 = $builder->build_sample_item(
1276 biblionumber => $biblio2->biblionumber,
1284 my %a_borrower_data = (
1285 firstname => 'Fridolyn',
1286 surname => 'SOMERS',
1287 categorycode => $patron_category->{categorycode},
1288 branchcode => $branch,
1291 my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1292 my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber )->unblessed;
1294 my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1295 my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1296 my $today = DateTime->today(time_zone => C4::Context->tz());
1298 my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1299 my $datedue = dt_from_string( $issue->date_due() );
1300 my $issue2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1301 my $datedue2 = dt_from_string( $issue->date_due() );
1305 # GetUpcomingDueIssues tests
1307 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1308 is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1311 #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1312 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1313 is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1316 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1317 is ( scalar( @$upcoming_dues ), 1,
1318 "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1321 # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1323 my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1325 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1326 is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1328 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1329 is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1331 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1332 is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1334 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1335 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1337 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1338 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1340 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1341 is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1345 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1346 my $branch = $library2->{branchcode};
1348 my $biblio = $builder->build_sample_biblio();
1351 my $item = $builder->build_sample_item(
1353 biblionumber => $biblio->biblionumber,
1360 my %a_borrower_data = (
1361 firstname => 'Kyle',
1363 categorycode => $patron_category->{categorycode},
1364 branchcode => $branch,
1367 my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1369 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1370 my $issue = AddIssue( $borrower, $item->barcode );
1373 issue_id => $issue->id(),
1374 itemnumber => $item->itemnumber,
1375 borrowernumber => $borrowernumber,
1381 my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1382 my $count = $hr->{count};
1384 is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1387 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1388 $dbh->do('DELETE FROM issues');
1389 $dbh->do('DELETE FROM items');
1390 $dbh->do('DELETE FROM circulation_rules');
1391 Koha::CirculationRules->set_rules(
1393 categorycode => undef,
1395 branchcode => undef,
1397 reservesallowed => 25,
1399 lengthunit => 'days',
1400 renewalsallowed => 1,
1402 norenewalbefore => undef,
1410 my $biblio = $builder->build_sample_biblio();
1412 my $item_1 = $builder->build_sample_item(
1414 biblionumber => $biblio->biblionumber,
1415 library => $library2->{branchcode},
1420 my $item_2= $builder->build_sample_item(
1422 biblionumber => $biblio->biblionumber,
1423 library => $library2->{branchcode},
1428 my $borrowernumber1 = Koha::Patron->new({
1429 firstname => 'Kyle',
1431 categorycode => $patron_category->{categorycode},
1432 branchcode => $library2->{branchcode},
1433 })->store->borrowernumber;
1434 my $borrowernumber2 = Koha::Patron->new({
1435 firstname => 'Chelsea',
1437 categorycode => $patron_category->{categorycode},
1438 branchcode => $library2->{branchcode},
1439 })->store->borrowernumber;
1441 my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
1442 my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
1444 my $issue = AddIssue( $borrower1, $item_1->barcode );
1446 my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1447 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1451 branchcode => $library2->{branchcode},
1452 borrowernumber => $borrowernumber2,
1453 biblionumber => $biblio->biblionumber,
1458 Koha::CirculationRules->set_rules(
1460 categorycode => undef,
1462 branchcode => undef,
1468 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1469 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1470 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1472 Koha::CirculationRules->set_rules(
1474 categorycode => undef,
1476 branchcode => undef,
1482 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1483 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1484 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1486 Koha::CirculationRules->set_rules(
1488 categorycode => undef,
1490 branchcode => undef,
1496 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1497 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1498 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1500 Koha::CirculationRules->set_rules(
1502 categorycode => undef,
1504 branchcode => undef,
1510 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1511 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1512 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1514 # Setting item not checked out to be not for loan but holdable
1515 $item_2->notforloan(-1)->store;
1517 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1518 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' );
1522 # Don't allow renewing onsite checkout
1523 my $branch = $library->{branchcode};
1525 #Create another record
1526 my $biblio = $builder->build_sample_biblio();
1528 my $item = $builder->build_sample_item(
1530 biblionumber => $biblio->biblionumber,
1536 my $borrowernumber = Koha::Patron->new({
1539 categorycode => $patron_category->{categorycode},
1540 branchcode => $branch,
1541 })->store->borrowernumber;
1543 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1545 my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
1546 my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $item->itemnumber );
1547 is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
1548 is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
1552 my $library = $builder->build({ source => 'Branch' });
1554 my $biblio = $builder->build_sample_biblio();
1556 my $item = $builder->build_sample_item(
1558 biblionumber => $biblio->biblionumber,
1559 library => $library->{branchcode},
1564 my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
1566 my $issue = AddIssue( $patron, $item->barcode );
1569 issue_id => $issue->id(),
1570 itemnumber => $item->itemnumber,
1571 borrowernumber => $patron->{borrowernumber},
1578 issue_id => $issue->id(),
1579 itemnumber => $item->itemnumber,
1580 borrowernumber => $patron->{borrowernumber},
1585 is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1588 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1591 my $homebranch = $builder->build( { source => 'Branch' } );
1592 my $holdingbranch = $builder->build( { source => 'Branch' } );
1593 my $otherbranch = $builder->build( { source => 'Branch' } );
1594 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1595 my $patron_2 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1597 my $item = $builder->build_sample_item(
1599 homebranch => $homebranch->{branchcode},
1600 holdingbranch => $holdingbranch->{branchcode},
1604 set_userenv($holdingbranch);
1606 my $issue = AddIssue( $patron_1->unblessed, $item->barcode );
1607 is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
1609 my ( $error, $question, $alerts );
1611 # AllowReturnToBranch == anywhere
1612 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1613 ## Test that unknown barcodes don't generate internal server errors
1614 set_userenv($homebranch);
1615 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
1616 ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
1617 ## Can be issued from homebranch
1618 set_userenv($homebranch);
1619 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1620 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1621 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1622 ## Can be issued from holdingbranch
1623 set_userenv($holdingbranch);
1624 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1625 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1626 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1627 ## Can be issued from another branch
1628 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1629 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1630 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1632 # AllowReturnToBranch == holdingbranch
1633 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1634 ## Cannot be issued from homebranch
1635 set_userenv($homebranch);
1636 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1637 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1638 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1639 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
1640 ## Can be issued from holdinbranch
1641 set_userenv($holdingbranch);
1642 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1643 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1644 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1645 ## Cannot be issued from another branch
1646 set_userenv($otherbranch);
1647 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1648 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1649 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1650 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
1652 # AllowReturnToBranch == homebranch
1653 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1654 ## Can be issued from holdinbranch
1655 set_userenv($homebranch);
1656 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1657 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1658 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1659 ## Cannot be issued from holdinbranch
1660 set_userenv($holdingbranch);
1661 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1662 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1663 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1664 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1665 ## Cannot be issued from holdinbranch
1666 set_userenv($otherbranch);
1667 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1668 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1669 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1670 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1672 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1675 subtest 'AddIssue & AllowReturnToBranch' => sub {
1678 my $homebranch = $builder->build( { source => 'Branch' } );
1679 my $holdingbranch = $builder->build( { source => 'Branch' } );
1680 my $otherbranch = $builder->build( { source => 'Branch' } );
1681 my $patron_1 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1682 my $patron_2 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1684 my $item = $builder->build_sample_item(
1686 homebranch => $homebranch->{branchcode},
1687 holdingbranch => $holdingbranch->{branchcode},
1691 set_userenv($holdingbranch);
1693 my $ref_issue = 'Koha::Checkout';
1694 my $issue = AddIssue( $patron_1, $item->barcode );
1696 my ( $error, $question, $alerts );
1698 # AllowReturnToBranch == homebranch
1699 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1700 ## Can be issued from homebranch
1701 set_userenv($homebranch);
1702 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
1703 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1704 ## Can be issued from holdinbranch
1705 set_userenv($holdingbranch);
1706 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
1707 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1708 ## Can be issued from another branch
1709 set_userenv($otherbranch);
1710 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
1711 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1713 # AllowReturnToBranch == holdinbranch
1714 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1715 ## Cannot be issued from homebranch
1716 set_userenv($homebranch);
1717 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
1718 ## Can be issued from holdingbranch
1719 set_userenv($holdingbranch);
1720 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
1721 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1722 ## Cannot be issued from another branch
1723 set_userenv($otherbranch);
1724 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
1726 # AllowReturnToBranch == homebranch
1727 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1728 ## Can be issued from homebranch
1729 set_userenv($homebranch);
1730 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
1731 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1732 ## Cannot be issued from holdinbranch
1733 set_userenv($holdingbranch);
1734 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
1735 ## Cannot be issued from another branch
1736 set_userenv($otherbranch);
1737 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
1738 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1741 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1744 my $library = $builder->build( { source => 'Branch' } );
1745 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1746 my $item_1 = $builder->build_sample_item(
1748 library => $library->{branchcode},
1751 my $item_2 = $builder->build_sample_item(
1753 library => $library->{branchcode},
1757 my ( $error, $question, $alerts );
1759 # Patron cannot issue item_1, they have overdues
1760 my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1761 my $issue = AddIssue( $patron->unblessed, $item_1->barcode, $yesterday ); # Add an overdue
1763 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1764 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1765 is( keys(%$error) + keys(%$alerts), 0, 'No key for error and alert' . str($error, $question, $alerts) );
1766 is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
1768 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1769 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1770 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1771 is( $error->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
1773 # Patron cannot issue item_1, they are debarred
1774 my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1775 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
1776 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1777 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1778 is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
1780 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
1781 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1782 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1783 is( $error->{USERBLOCKEDNOENDDATE}, '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
1786 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
1789 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1790 my $patron_category_x = $builder->build_object(
1792 class => 'Koha::Patron::Categories',
1793 value => { category_type => 'X' }
1796 my $patron = $builder->build_object(
1798 class => 'Koha::Patrons',
1800 categorycode => $patron_category_x->categorycode,
1801 gonenoaddress => undef,
1808 my $item_1 = $builder->build_sample_item(
1810 library => $library->{branchcode},
1814 my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->barcode );
1815 is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
1817 # TODO There are other tests to provide here
1820 subtest 'MultipleReserves' => sub {
1823 my $biblio = $builder->build_sample_biblio();
1825 my $branch = $library2->{branchcode};
1827 my $item_1 = $builder->build_sample_item(
1829 biblionumber => $biblio->biblionumber,
1831 replacementprice => 12.00,
1836 my $item_2 = $builder->build_sample_item(
1838 biblionumber => $biblio->biblionumber,
1840 replacementprice => 12.00,
1847 my $resdate = undef;
1848 my $expdate = undef;
1850 my $checkitem = undef;
1853 my %renewing_borrower_data = (
1854 firstname => 'John',
1855 surname => 'Renewal',
1856 categorycode => $patron_category->{categorycode},
1857 branchcode => $branch,
1859 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
1860 my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
1861 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
1862 my $datedue = dt_from_string( $issue->date_due() );
1863 is (defined $issue->date_due(), 1, "item 1 checked out");
1864 my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
1866 my %reserving_borrower_data1 = (
1867 firstname => 'Katrin',
1868 surname => 'Reservation',
1869 categorycode => $patron_category->{categorycode},
1870 branchcode => $branch,
1872 my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
1875 branchcode => $branch,
1876 borrowernumber => $reserving_borrowernumber1,
1877 biblionumber => $biblio->biblionumber,
1878 priority => $priority,
1879 reservation_date => $resdate,
1880 expiration_date => $expdate,
1882 itemnumber => $checkitem,
1887 my %reserving_borrower_data2 = (
1888 firstname => 'Kirk',
1889 surname => 'Reservation',
1890 categorycode => $patron_category->{categorycode},
1891 branchcode => $branch,
1893 my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
1896 branchcode => $branch,
1897 borrowernumber => $reserving_borrowernumber2,
1898 biblionumber => $biblio->biblionumber,
1899 priority => $priority,
1900 reservation_date => $resdate,
1901 expiration_date => $expdate,
1903 itemnumber => $checkitem,
1909 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1910 is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
1913 my $item_3 = $builder->build_sample_item(
1915 biblionumber => $biblio->biblionumber,
1917 replacementprice => 12.00,
1923 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1924 is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
1928 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
1931 my $library = $builder->build( { source => 'Branch' } );
1932 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1934 my $biblionumber = $builder->build_sample_biblio(
1936 branchcode => $library->{branchcode},
1939 my $item_1 = $builder->build_sample_item(
1941 biblionumber => $biblionumber,
1942 library => $library->{branchcode},
1946 my $item_2 = $builder->build_sample_item(
1948 biblionumber => $biblionumber,
1949 library => $library->{branchcode},
1953 my ( $error, $question, $alerts );
1954 my $issue = AddIssue( $patron->unblessed, $item_1->barcode, dt_from_string->add( days => 1 ) );
1956 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1957 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1959 { error => $error, alerts => $alerts },
1960 { error => {}, alerts => {} },
1961 'No error or alert should be raised'
1963 is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
1965 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1966 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1968 { error => $error, question => $question, alerts => $alerts },
1969 { error => {}, question => {}, alerts => {} },
1970 'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
1973 # Add a subscription
1974 Koha::Subscription->new({ biblionumber => $biblionumber })->store;
1976 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1977 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1979 { error => $error, question => $question, alerts => $alerts },
1980 { error => {}, question => {}, alerts => {} },
1981 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
1984 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
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'
1993 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
1996 my $library = $builder->build( { source => 'Branch' } );
1997 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2000 my $biblionumber = $builder->build_sample_biblio(
2002 branchcode => $library->{branchcode},
2005 my $item_1 = $builder->build_sample_item(
2007 biblionumber => $biblionumber,
2008 library => $library->{branchcode},
2011 my $item_2 = $builder->build_sample_item(
2013 biblionumber => $biblionumber,
2014 library => $library->{branchcode},
2018 # And the circulation rule
2019 Koha::CirculationRules->search->delete;
2020 Koha::CirculationRules->set_rules(
2022 categorycode => undef,
2024 branchcode => undef,
2027 firstremind => 1, # 1 day of grace
2028 finedays => 2, # 2 days of fine per day of overdue
2029 lengthunit => 'days',
2034 # Patron cannot issue item_1, they have overdues
2035 my $now = dt_from_string;
2036 my $five_days_ago = $now->clone->subtract( days => 5 );
2037 my $ten_days_ago = $now->clone->subtract( days => 10 );
2038 AddIssue( $patron, $item_1->barcode, $five_days_ago ); # Add an overdue
2039 AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2040 ; # Add another overdue
2042 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2043 AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2044 my $debarments = Koha::Patron::Debarments::GetDebarments(
2045 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2046 is( scalar(@$debarments), 1 );
2048 # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2049 # Same for the others
2050 my $expected_expiration = output_pref(
2052 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2053 dateformat => 'sql',
2057 is( $debarments->[0]->{expiration}, $expected_expiration );
2059 AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2060 $debarments = Koha::Patron::Debarments::GetDebarments(
2061 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2062 is( scalar(@$debarments), 1 );
2063 $expected_expiration = output_pref(
2065 dt => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2066 dateformat => 'sql',
2070 is( $debarments->[0]->{expiration}, $expected_expiration );
2072 Koha::Patron::Debarments::DelUniqueDebarment(
2073 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2075 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2076 AddIssue( $patron, $item_1->barcode, $five_days_ago ); # Add an overdue
2077 AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2078 ; # Add another overdue
2079 AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2080 $debarments = Koha::Patron::Debarments::GetDebarments(
2081 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2082 is( scalar(@$debarments), 1 );
2083 $expected_expiration = output_pref(
2085 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2086 dateformat => 'sql',
2090 is( $debarments->[0]->{expiration}, $expected_expiration );
2092 AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2093 $debarments = Koha::Patron::Debarments::GetDebarments(
2094 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2095 is( scalar(@$debarments), 1 );
2096 $expected_expiration = output_pref(
2098 dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
2099 dateformat => 'sql',
2103 is( $debarments->[0]->{expiration}, $expected_expiration );
2106 subtest 'AddReturn + suspension_chargeperiod' => sub {
2109 my $library = $builder->build( { source => 'Branch' } );
2110 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2112 my $biblionumber = $builder->build_sample_biblio(
2114 branchcode => $library->{branchcode},
2117 my $item_1 = $builder->build_sample_item(
2119 biblionumber => $biblionumber,
2120 library => $library->{branchcode},
2124 # And the issuing rule
2125 Koha::CirculationRules->search->delete;
2126 Koha::CirculationRules->set_rules(
2128 categorycode => '*',
2133 firstremind => 0, # 0 day of grace
2134 finedays => 2, # 2 days of fine per day of overdue
2135 suspension_chargeperiod => 1,
2136 lengthunit => 'days',
2141 my $now = dt_from_string;
2142 my $five_days_ago = $now->clone->subtract( days => 5 );
2143 # We want to charge 2 days every day, without grace
2144 # With 5 days of overdue: 5 * Z
2145 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2146 test_debarment_on_checkout(
2149 library => $library,
2151 due_date => $five_days_ago,
2152 expiration_date => $expected_expiration,
2156 # Same with undef firstremind
2157 Koha::CirculationRules->search->delete;
2158 Koha::CirculationRules->set_rules(
2160 categorycode => '*',
2165 firstremind => undef, # 0 day of grace
2166 finedays => 2, # 2 days of fine per day of overdue
2167 suspension_chargeperiod => 1,
2168 lengthunit => 'days',
2173 my $now = dt_from_string;
2174 my $five_days_ago = $now->clone->subtract( days => 5 );
2175 # We want to charge 2 days every day, without grace
2176 # With 5 days of overdue: 5 * Z
2177 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2178 test_debarment_on_checkout(
2181 library => $library,
2183 due_date => $five_days_ago,
2184 expiration_date => $expected_expiration,
2188 # We want to charge 2 days every 2 days, without grace
2189 # With 5 days of overdue: (5 * 2) / 2
2190 Koha::CirculationRules->set_rule(
2192 categorycode => undef,
2193 branchcode => undef,
2195 rule_name => 'suspension_chargeperiod',
2200 $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
2201 test_debarment_on_checkout(
2204 library => $library,
2206 due_date => $five_days_ago,
2207 expiration_date => $expected_expiration,
2211 # We want to charge 2 days every 3 days, with 1 day of grace
2212 # With 5 days of overdue: ((5-1) / 3 ) * 2
2213 Koha::CirculationRules->set_rules(
2215 categorycode => undef,
2216 branchcode => undef,
2219 suspension_chargeperiod => 3,
2224 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
2225 test_debarment_on_checkout(
2228 library => $library,
2230 due_date => $five_days_ago,
2231 expiration_date => $expected_expiration,
2235 # Use finesCalendar to know if holiday must be skipped to calculate the due date
2236 # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
2237 Koha::CirculationRules->set_rules(
2239 categorycode => undef,
2240 branchcode => undef,
2244 suspension_chargeperiod => 1,
2249 t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
2250 t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
2252 # Adding a holiday 2 days ago
2253 my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
2254 my $two_days_ago = $now->clone->subtract( days => 2 );
2255 $calendar->insert_single_holiday(
2256 day => $two_days_ago->day,
2257 month => $two_days_ago->month,
2258 year => $two_days_ago->year,
2259 title => 'holidayTest-2d',
2260 description => 'holidayDesc 2 days ago'
2262 # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
2263 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
2264 test_debarment_on_checkout(
2267 library => $library,
2269 due_date => $five_days_ago,
2270 expiration_date => $expected_expiration,
2274 # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
2275 my $two_days_ahead = $now->clone->add( days => 2 );
2276 $calendar->insert_single_holiday(
2277 day => $two_days_ahead->day,
2278 month => $two_days_ahead->month,
2279 year => $two_days_ahead->year,
2280 title => 'holidayTest+2d',
2281 description => 'holidayDesc 2 days ahead'
2284 # Same as above, but we should skip D+2
2285 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
2286 test_debarment_on_checkout(
2289 library => $library,
2291 due_date => $five_days_ago,
2292 expiration_date => $expected_expiration,
2296 # Adding another holiday, day of expiration date
2297 my $expected_expiration_dt = dt_from_string($expected_expiration);
2298 $calendar->insert_single_holiday(
2299 day => $expected_expiration_dt->day,
2300 month => $expected_expiration_dt->month,
2301 year => $expected_expiration_dt->year,
2302 title => 'holidayTest_exp',
2303 description => 'holidayDesc on expiration date'
2305 # Expiration date will be the day after
2306 test_debarment_on_checkout(
2309 library => $library,
2311 due_date => $five_days_ago,
2312 expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
2316 test_debarment_on_checkout(
2319 library => $library,
2321 return_date => $now->clone->add(days => 5),
2322 expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
2326 test_debarment_on_checkout(
2329 library => $library,
2331 due_date => $now->clone->add(days => 1),
2332 return_date => $now->clone->add(days => 5),
2333 expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
2339 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
2342 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2343 my $patron1 = $builder->build_object(
2345 class => 'Koha::Patrons',
2347 library => $library->branchcode,
2348 categorycode => $patron_category->{categorycode}
2352 my $patron2 = $builder->build_object(
2354 class => 'Koha::Patrons',
2356 library => $library->branchcode,
2357 categorycode => $patron_category->{categorycode}
2362 t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
2364 my $item = $builder->build_sample_item(
2366 library => $library->branchcode,
2370 my ( $error, $question, $alerts );
2371 my $issue = AddIssue( $patron1->unblessed, $item->barcode );
2373 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2374 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
2375 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' );
2377 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
2378 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
2379 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' );
2381 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2385 subtest 'AddReturn | is_overdue' => sub {
2388 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
2389 t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
2390 t::lib::Mocks::mock_preference('finesMode', 'production');
2391 t::lib::Mocks::mock_preference('MaxFine', '100');
2393 my $library = $builder->build( { source => 'Branch' } );
2394 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2395 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2396 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
2398 my $item = $builder->build_sample_item(
2400 library => $library->{branchcode},
2401 replacementprice => 7
2405 Koha::CirculationRules->search->delete;
2406 Koha::CirculationRules->set_rules(
2408 categorycode => undef,
2410 branchcode => undef,
2413 lengthunit => 'days',
2414 fine => 1, # Charge 1 every day of overdue
2420 my $now = dt_from_string;
2421 my $one_day_ago = $now->clone->subtract( days => 1 );
2422 my $two_days_ago = $now->clone->subtract( days => 2 );
2423 my $five_days_ago = $now->clone->subtract( days => 5 );
2424 my $ten_days_ago = $now->clone->subtract( days => 10 );
2425 $patron = Koha::Patrons->find( $patron->{borrowernumber} );
2427 # No return date specified, today will be used => 10 days overdue charged
2428 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2429 AddReturn( $item->barcode, $library->{branchcode} );
2430 is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
2431 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2433 # specify return date 5 days before => no overdue charged
2434 AddIssue( $patron->unblessed, $item->barcode, $five_days_ago ); # date due was 5d ago
2435 AddReturn( $item->barcode, $library->{branchcode}, undef, $ten_days_ago );
2436 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2437 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2439 # specify return date 5 days later => 5 days overdue charged
2440 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2441 AddReturn( $item->barcode, $library->{branchcode}, undef, $five_days_ago );
2442 is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
2443 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2445 # specify return date 5 days later, specify exemptfine => no overdue charge
2446 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2447 AddReturn( $item->barcode, $library->{branchcode}, 1, $five_days_ago );
2448 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2449 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2451 subtest 'bug 22877 | Lost item return' => sub {
2455 my $issue = AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2457 # Fake fines cronjob on this checkout
2459 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2460 $ten_days_ago, $now );
2463 issue_id => $issue->issue_id,
2464 itemnumber => $item->itemnumber,
2465 borrowernumber => $patron->borrowernumber,
2467 due => output_pref($ten_days_ago)
2470 is( int( $patron->account->balance() ),
2471 10, "Overdue fine of 10 days overdue" );
2473 # Fake longoverdue with charge and not marking returned
2474 LostItem( $item->itemnumber, 'cronjob', 0 );
2475 is( int( $patron->account->balance() ),
2476 17, "Lost fine of 7 plus 10 days overdue" );
2478 # Now we return it today
2479 AddReturn( $item->barcode, $library->{branchcode} );
2480 is( int( $patron->account->balance() ),
2481 17, "Should have a single 10 days overdue fine and lost charge" );
2484 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2487 subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
2491 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2493 my $issue = AddIssue( $patron->unblessed, $item->barcode, $one_day_ago ); # date due was 1d ago
2495 # Fake fines cronjob on this checkout
2497 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2498 $one_day_ago, $now );
2501 issue_id => $issue->issue_id,
2502 itemnumber => $item->itemnumber,
2503 borrowernumber => $patron->borrowernumber,
2505 due => output_pref($one_day_ago)
2508 is( int( $patron->account->balance() ),
2509 1, "Overdue fine of 1 day overdue" );
2511 # Backdated return (dropbox mode example - charge should be removed)
2512 AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
2513 is( int( $patron->account->balance() ),
2514 0, "Overdue fine should be annulled" );
2515 my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2516 is( $lines->count, 0, "Overdue fine accountline has been removed");
2518 $issue = AddIssue( $patron->unblessed, $item->barcode, $two_days_ago ); # date due was 2d ago
2520 # Fake fines cronjob on this checkout
2522 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2523 $two_days_ago, $now );
2526 issue_id => $issue->issue_id,
2527 itemnumber => $item->itemnumber,
2528 borrowernumber => $patron->borrowernumber,
2530 due => output_pref($one_day_ago)
2533 is( int( $patron->account->balance() ),
2534 2, "Overdue fine of 2 days overdue" );
2536 # Payment made against fine
2537 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2538 my $debit = $lines->next;
2539 my $credit = $patron->account->add_credit(
2543 interface => 'test',
2547 { debits => [ $debit ], offset_type => 'Payment' } );
2549 is( int( $patron->account->balance() ),
2550 0, "Overdue fine should be paid off" );
2551 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2552 is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
2553 my $line = $lines->next;
2554 is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
2555 is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
2557 # Backdated return (dropbox mode example - charge should be removed)
2558 AddReturn( $item->barcode, $library->{branchcode}, undef, $one_day_ago );
2559 is( int( $patron->account->balance() ),
2560 -1, "Refund credit has been applied" );
2561 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
2562 is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
2564 $line = $lines->next;
2565 is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
2566 is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
2567 is($line->status,'RETURNED', "Overdue fine is fixed");
2568 $line = $lines->next;
2569 is($line->amount+0,-2, "Original payment amount remains as 2");
2570 is($line->amountoutstanding+0,0, "Original payment remains applied");
2571 $line = $lines->next;
2572 is($line->amount+0,-1, "Refund amount correctly set to 1");
2573 is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
2576 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2579 subtest 'bug 25417 | backdated return + exemptfine' => sub {
2583 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2585 my $issue = AddIssue( $patron->unblessed, $item->barcode, $one_day_ago ); # date due was 1d ago
2587 # Fake fines cronjob on this checkout
2589 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2590 $one_day_ago, $now );
2593 issue_id => $issue->issue_id,
2594 itemnumber => $item->itemnumber,
2595 borrowernumber => $patron->borrowernumber,
2597 due => output_pref($one_day_ago)
2600 is( int( $patron->account->balance() ),
2601 1, "Overdue fine of 1 day overdue" );
2603 # Backdated return (dropbox mode example - charge should no longer exist)
2604 AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
2605 is( int( $patron->account->balance() ),
2606 0, "Overdue fine should be annulled" );
2609 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2612 subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
2615 t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
2617 my $due_date = dt_from_string;
2618 my $issue = AddIssue( $patron->unblessed, $item->barcode, $due_date );
2623 issue_id => $issue->issue_id,
2624 itemnumber => $item->itemnumber,
2625 borrowernumber => $patron->borrowernumber,
2627 due => output_pref($due_date)
2630 is( $patron->account->balance(),
2631 0.25, 'Overdue fine of $0.25 recorded' );
2633 # Backdate return to exact due date and time
2634 my ( undef, $message ) =
2635 AddReturn( $item->barcode, $library->{branchcode},
2639 Koha::Account::Lines->find( { issue_id => $issue->id } );
2640 ok( !$accountline, 'accountline removed as expected' );
2643 $issue = AddIssue( $patron->unblessed, $item->barcode, $due_date );
2648 issue_id => $issue->issue_id,
2649 itemnumber => $item->itemnumber,
2650 borrowernumber => $patron->borrowernumber,
2652 due => output_pref($due_date)
2655 is( $patron->account->balance(),
2656 0.25, 'Overdue fine of $0.25 recorded' );
2658 # Partial pay accruing fine
2659 my $lines = Koha::Account::Lines->search(
2661 borrowernumber => $patron->borrowernumber,
2662 issue_id => $issue->id
2665 my $debit = $lines->next;
2666 my $credit = $patron->account->add_credit(
2670 interface => 'test',
2673 $credit->apply( { debits => [$debit], offset_type => 'Payment' } );
2675 is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
2677 # Backdate return to exact due date and time
2678 ( undef, $message ) =
2679 AddReturn( $item->barcode, $library->{branchcode},
2682 $lines = Koha::Account::Lines->search(
2684 borrowernumber => $patron->borrowernumber,
2685 issue_id => $issue->id
2688 $accountline = $lines->next;
2689 is( $accountline->amountoutstanding + 0,
2690 0, 'Partially paid fee amount outstanding was reduced to 0' );
2691 is( $accountline->amount + 0,
2692 0, 'Partially paid fee amount was reduced to 0' );
2693 is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
2696 Koha::Account::Lines->search(
2697 { borrowernumber => $patron->borrowernumber } )->delete;
2700 subtest 'enh 23091 | Lost item return policies' => sub {
2703 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2705 my $branchcode_false =
2706 $builder->build( { source => 'Branch' } )->{branchcode};
2707 my $specific_rule_false = $builder->build(
2709 source => 'CirculationRule',
2711 branchcode => $branchcode_false,
2712 categorycode => undef,
2714 rule_name => 'lostreturn',
2719 my $branchcode_refund =
2720 $builder->build( { source => 'Branch' } )->{branchcode};
2721 my $specific_rule_refund = $builder->build(
2723 source => 'CirculationRule',
2725 branchcode => $branchcode_refund,
2726 categorycode => undef,
2728 rule_name => 'lostreturn',
2729 rule_value => 'refund'
2733 my $branchcode_restore =
2734 $builder->build( { source => 'Branch' } )->{branchcode};
2735 my $specific_rule_restore = $builder->build(
2737 source => 'CirculationRule',
2739 branchcode => $branchcode_restore,
2740 categorycode => undef,
2742 rule_name => 'lostreturn',
2743 rule_value => 'restore'
2747 my $branchcode_charge =
2748 $builder->build( { source => 'Branch' } )->{branchcode};
2749 my $specific_rule_charge = $builder->build(
2751 source => 'CirculationRule',
2753 branchcode => $branchcode_charge,
2754 categorycode => undef,
2756 rule_name => 'lostreturn',
2757 rule_value => 'charge'
2762 my $replacement_amount = 99.00;
2763 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2764 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
2765 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
2766 t::lib::Mocks::mock_preference( 'BlockReturnOfLostItems', 0 );
2767 t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl',
2769 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge',
2772 subtest 'lostreturn | false' => sub {
2775 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_false });
2777 my $item = $builder->build_sample_item(
2779 replacementprice => $replacement_amount
2784 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
2786 # Fake fines cronjob on this checkout
2788 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2789 $ten_days_ago, $now );
2792 issue_id => $issue->issue_id,
2793 itemnumber => $item->itemnumber,
2794 borrowernumber => $patron->borrowernumber,
2796 due => output_pref($ten_days_ago)
2799 my $overdue_fees = Koha::Account::Lines->search(
2801 borrowernumber => $patron->id,
2802 itemnumber => $item->itemnumber,
2803 debit_type_code => 'OVERDUE'
2806 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
2807 my $overdue_fee = $overdue_fees->next;
2808 is( $overdue_fee->amount + 0,
2809 10, 'The right OVERDUE amount is generated' );
2810 is( $overdue_fee->amountoutstanding + 0,
2812 'The right OVERDUE amountoutstanding is generated' );
2814 # Simulate item marked as lost
2815 $item->itemlost(3)->store;
2816 C4::Circulation::LostItem( $item->itemnumber, 1 );
2818 my $lost_fee_lines = Koha::Account::Lines->search(
2820 borrowernumber => $patron->id,
2821 itemnumber => $item->itemnumber,
2822 debit_type_code => 'LOST'
2825 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
2826 my $lost_fee_line = $lost_fee_lines->next;
2827 is( $lost_fee_line->amount + 0,
2828 $replacement_amount, 'The right LOST amount is generated' );
2829 is( $lost_fee_line->amountoutstanding + 0,
2830 $replacement_amount,
2831 'The right LOST amountoutstanding is generated' );
2832 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2835 my ( $returned, $message ) =
2836 AddReturn( $item->barcode, $branchcode_false, undef, $five_days_ago );
2838 $overdue_fee->discard_changes;
2839 is( $overdue_fee->amount + 0,
2840 10, 'The OVERDUE amount is left intact' );
2841 is( $overdue_fee->amountoutstanding + 0,
2843 'The OVERDUE amountoutstanding is left intact' );
2845 $lost_fee_line->discard_changes;
2846 is( $lost_fee_line->amount + 0,
2847 $replacement_amount, 'The LOST amount is left intact' );
2848 is( $lost_fee_line->amountoutstanding + 0,
2849 $replacement_amount,
2850 'The LOST amountoutstanding is left intact' );
2851 # FIXME: Should we set the LOST fee status to 'FOUND' regardless of whether we're refunding or not?
2852 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2855 subtest 'lostreturn | refund' => sub {
2858 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund });
2860 my $item = $builder->build_sample_item(
2862 replacementprice => $replacement_amount
2867 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
2869 # Fake fines cronjob on this checkout
2871 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2872 $ten_days_ago, $now );
2875 issue_id => $issue->issue_id,
2876 itemnumber => $item->itemnumber,
2877 borrowernumber => $patron->borrowernumber,
2879 due => output_pref($ten_days_ago)
2882 my $overdue_fees = Koha::Account::Lines->search(
2884 borrowernumber => $patron->id,
2885 itemnumber => $item->itemnumber,
2886 debit_type_code => 'OVERDUE'
2889 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
2890 my $overdue_fee = $overdue_fees->next;
2891 is( $overdue_fee->amount + 0,
2892 10, 'The right OVERDUE amount is generated' );
2893 is( $overdue_fee->amountoutstanding + 0,
2895 'The right OVERDUE amountoutstanding is generated' );
2897 # Simulate item marked as lost
2898 $item->itemlost(3)->store;
2899 C4::Circulation::LostItem( $item->itemnumber, 1 );
2901 my $lost_fee_lines = Koha::Account::Lines->search(
2903 borrowernumber => $patron->id,
2904 itemnumber => $item->itemnumber,
2905 debit_type_code => 'LOST'
2908 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
2909 my $lost_fee_line = $lost_fee_lines->next;
2910 is( $lost_fee_line->amount + 0,
2911 $replacement_amount, 'The right LOST amount is generated' );
2912 is( $lost_fee_line->amountoutstanding + 0,
2913 $replacement_amount,
2914 'The right LOST amountoutstanding is generated' );
2915 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2917 # Return the lost item
2918 my ( undef, $message ) =
2919 AddReturn( $item->barcode, $branchcode_refund, undef, $five_days_ago );
2921 $overdue_fee->discard_changes;
2922 is( $overdue_fee->amount + 0,
2923 10, 'The OVERDUE amount is left intact' );
2924 is( $overdue_fee->amountoutstanding + 0,
2926 'The OVERDUE amountoutstanding is left intact' );
2928 $lost_fee_line->discard_changes;
2929 is( $lost_fee_line->amount + 0,
2930 $replacement_amount, 'The LOST amount is left intact' );
2931 is( $lost_fee_line->amountoutstanding + 0,
2933 'The LOST amountoutstanding is refunded' );
2934 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
2937 subtest 'lostreturn | restore' => sub {
2940 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_restore });
2942 my $item = $builder->build_sample_item(
2944 replacementprice => $replacement_amount
2949 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode , $ten_days_ago);
2951 # Fake fines cronjob on this checkout
2953 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2954 $ten_days_ago, $now );
2957 issue_id => $issue->issue_id,
2958 itemnumber => $item->itemnumber,
2959 borrowernumber => $patron->borrowernumber,
2961 due => output_pref($ten_days_ago)
2964 my $overdue_fees = Koha::Account::Lines->search(
2966 borrowernumber => $patron->id,
2967 itemnumber => $item->itemnumber,
2968 debit_type_code => 'OVERDUE'
2971 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
2972 my $overdue_fee = $overdue_fees->next;
2973 is( $overdue_fee->amount + 0,
2974 10, 'The right OVERDUE amount is generated' );
2975 is( $overdue_fee->amountoutstanding + 0,
2977 'The right OVERDUE amountoutstanding is generated' );
2979 # Simulate item marked as lost
2980 $item->itemlost(3)->store;
2981 C4::Circulation::LostItem( $item->itemnumber, 1 );
2983 my $lost_fee_lines = Koha::Account::Lines->search(
2985 borrowernumber => $patron->id,
2986 itemnumber => $item->itemnumber,
2987 debit_type_code => 'LOST'
2990 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
2991 my $lost_fee_line = $lost_fee_lines->next;
2992 is( $lost_fee_line->amount + 0,
2993 $replacement_amount, 'The right LOST amount is generated' );
2994 is( $lost_fee_line->amountoutstanding + 0,
2995 $replacement_amount,
2996 'The right LOST amountoutstanding is generated' );
2997 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2999 # Simulate refunding overdue fees upon marking item as lost
3000 my $overdue_forgive = $patron->account->add_credit(
3003 user_id => $manager->borrowernumber,
3004 library_id => $branchcode_restore,
3005 interface => 'test',
3007 item_id => $item->itemnumber
3010 $overdue_forgive->apply(
3011 { debits => [$overdue_fee], offset_type => 'Forgiven' } );
3012 $overdue_fee->discard_changes;
3013 is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
3016 my ( undef, $message ) =
3017 AddReturn( $item->barcode, $branchcode_restore, undef, $five_days_ago );
3019 $overdue_fee->discard_changes;
3020 is( $overdue_fee->amount + 0,
3021 10, 'The OVERDUE amount is left intact' );
3022 is( $overdue_fee->amountoutstanding + 0,
3024 'The OVERDUE amountoutstanding is restored' );
3026 $lost_fee_line->discard_changes;
3027 is( $lost_fee_line->amount + 0,
3028 $replacement_amount, 'The LOST amount is left intact' );
3029 is( $lost_fee_line->amountoutstanding + 0,
3031 'The LOST amountoutstanding is refunded' );
3032 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3035 subtest 'lostreturn | charge' => sub {
3038 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_charge });
3040 my $item = $builder->build_sample_item(
3042 replacementprice => $replacement_amount
3047 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
3049 # Fake fines cronjob on this checkout
3051 CalcFine( $item, $patron->categorycode, $library->{branchcode},
3052 $ten_days_ago, $now );
3055 issue_id => $issue->issue_id,
3056 itemnumber => $item->itemnumber,
3057 borrowernumber => $patron->borrowernumber,
3059 due => output_pref($ten_days_ago)
3062 my $overdue_fees = Koha::Account::Lines->search(
3064 borrowernumber => $patron->id,
3065 itemnumber => $item->itemnumber,
3066 debit_type_code => 'OVERDUE'
3069 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3070 my $overdue_fee = $overdue_fees->next;
3071 is( $overdue_fee->amount + 0,
3072 10, 'The right OVERDUE amount is generated' );
3073 is( $overdue_fee->amountoutstanding + 0,
3075 'The right OVERDUE amountoutstanding is generated' );
3077 # Simulate item marked as lost
3078 $item->itemlost(3)->store;
3079 C4::Circulation::LostItem( $item->itemnumber, 1 );
3081 my $lost_fee_lines = Koha::Account::Lines->search(
3083 borrowernumber => $patron->id,
3084 itemnumber => $item->itemnumber,
3085 debit_type_code => 'LOST'
3088 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3089 my $lost_fee_line = $lost_fee_lines->next;
3090 is( $lost_fee_line->amount + 0,
3091 $replacement_amount, 'The right LOST amount is generated' );
3092 is( $lost_fee_line->amountoutstanding + 0,
3093 $replacement_amount,
3094 'The right LOST amountoutstanding is generated' );
3095 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3097 # Simulate refunding overdue fees upon marking item as lost
3098 my $overdue_forgive = $patron->account->add_credit(
3101 user_id => $manager->borrowernumber,
3102 library_id => $branchcode_charge,
3103 interface => 'test',
3105 item_id => $item->itemnumber
3108 $overdue_forgive->apply(
3109 { debits => [$overdue_fee], offset_type => 'Forgiven' } );
3110 $overdue_fee->discard_changes;
3111 is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
3114 my ( undef, $message ) =
3115 AddReturn( $item->barcode, $branchcode_charge, undef, $five_days_ago );
3117 $lost_fee_line->discard_changes;
3118 is( $lost_fee_line->amount + 0,
3119 $replacement_amount, 'The LOST amount is left intact' );
3120 is( $lost_fee_line->amountoutstanding + 0,
3122 'The LOST amountoutstanding is refunded' );
3123 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3125 $overdue_fees = Koha::Account::Lines->search(
3127 borrowernumber => $patron->id,
3128 itemnumber => $item->itemnumber,
3129 debit_type_code => 'OVERDUE'
3132 order_by => { '-asc' => 'accountlines_id'}
3135 is( $overdue_fees->count, 2, 'A second OVERDUE fee has been added' );
3136 $overdue_fee = $overdue_fees->next;
3137 is( $overdue_fee->amount + 0,
3138 10, 'The original OVERDUE amount is left intact' );
3139 is( $overdue_fee->amountoutstanding + 0,
3141 'The original OVERDUE amountoutstanding is left as forgiven' );
3142 $overdue_fee = $overdue_fees->next;
3143 is( $overdue_fee->amount + 0,
3144 5, 'The new OVERDUE amount is correct for the backdated return' );
3145 is( $overdue_fee->amountoutstanding + 0,
3147 'The new OVERDUE amountoutstanding is correct for the backdated return' );
3152 subtest '_FixOverduesOnReturn' => sub {
3155 my $manager = $builder->build_object({ class => "Koha::Patrons" });
3156 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3158 my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
3160 my $branchcode = $library2->{branchcode};
3162 my $item = $builder->build_sample_item(
3164 biblionumber => $biblio->biblionumber,
3165 library => $branchcode,
3166 replacementprice => 99.00,
3171 my $patron = $builder->build( { source => 'Borrower' } );
3173 ## Start with basic call, should just close out the open fine
3174 my $accountline = Koha::Account::Line->new(
3176 borrowernumber => $patron->{borrowernumber},
3177 debit_type_code => 'OVERDUE',
3178 status => 'UNRETURNED',
3179 itemnumber => $item->itemnumber,
3181 amountoutstanding => 99.00,
3182 interface => 'test',
3186 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
3188 $accountline->_result()->discard_changes();
3190 is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
3191 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3192 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3194 ## Run again, with exemptfine enabled
3197 debit_type_code => 'OVERDUE',
3198 status => 'UNRETURNED',
3199 amountoutstanding => 99.00,
3203 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3205 $accountline->_result()->discard_changes();
3206 my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3208 is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
3209 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3210 is( $accountline->status, 'RETURNED', 'Open fine ( account type OVERDUE ) has been set to returned ( status RETURNED )');
3211 is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
3212 is( $offset->amount + 0, -99, "Amount of offset is correct" );
3213 my $credit = $offset->credit;
3214 is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
3215 is( $credit->amount + 0, -99, "Credit amount is set correctly" );
3216 is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
3218 # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
3221 debit_type_code => 'OVERDUE',
3222 status => 'UNRETURNED',
3223 amountoutstanding => 0.00,
3228 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3230 $accountline->_result()->discard_changes();
3231 $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3232 is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
3233 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3234 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3237 subtest 'Set waiting flag' => sub {
3240 my $library_1 = $builder->build( { source => 'Branch' } );
3241 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3242 my $library_2 = $builder->build( { source => 'Branch' } );
3243 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3245 my $item = $builder->build_sample_item(
3247 library => $library_1->{branchcode},
3251 set_userenv( $library_2 );
3252 my $reserve_id = AddReserve(
3254 branchcode => $library_2->{branchcode},
3255 borrowernumber => $patron_2->{borrowernumber},
3256 biblionumber => $item->biblionumber,
3258 itemnumber => $item->itemnumber,
3262 set_userenv( $library_1 );
3263 my $do_transfer = 1;
3264 my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3265 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3266 my $hold = Koha::Holds->find( $reserve_id );
3267 is( $hold->found, 'T', 'Hold is in transit' );
3269 my ( $status ) = CheckReserves($item->itemnumber);
3270 is( $status, 'Reserved', 'Hold is not waiting yet');
3272 set_userenv( $library_2 );
3274 AddReturn( $item->barcode, $library_2->{branchcode} );
3275 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3276 $hold = Koha::Holds->find( $reserve_id );
3277 is( $hold->found, 'W', 'Hold is waiting' );
3278 ( $status ) = CheckReserves($item->itemnumber);
3279 is( $status, 'Waiting', 'Now the hold is waiting');
3281 #Bug 21944 - Waiting transfer checked in at branch other than pickup location
3282 set_userenv( $library_1 );
3283 (undef, my $messages, undef, undef ) = AddReturn ( $item->barcode, $library_1->{branchcode} );
3284 $hold = Koha::Holds->find( $reserve_id );
3285 is( $hold->found, undef, 'Hold is no longer marked waiting' );
3286 is( $hold->priority, 1, "Hold is now priority one again");
3287 is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
3288 is( $hold->itemnumber, $item->itemnumber, "Hold has retained its' itemnumber");
3289 is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
3290 is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
3291 is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
3294 subtest 'Cancel transfers on lost items' => sub {
3296 my $library_1 = $builder->build( { source => 'Branch' } );
3297 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3298 my $library_2 = $builder->build( { source => 'Branch' } );
3299 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3300 my $biblio = $builder->build_sample_biblio({branchcode => $library->{branchcode}});
3301 my $item = $builder->build_sample_item({
3302 biblionumber => $biblio->biblionumber,
3303 library => $library_1->{branchcode},
3306 set_userenv( $library_2 );
3307 my $reserve_id = AddReserve(
3309 branchcode => $library_2->{branchcode},
3310 borrowernumber => $patron_2->{borrowernumber},
3311 biblionumber => $item->biblionumber,
3313 itemnumber => $item->itemnumber,
3317 #Return book and add transfer
3318 set_userenv( $library_1 );
3319 my $do_transfer = 1;
3320 my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3321 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3322 C4::Circulation::transferbook({
3323 from_branch => $library_1->{branchcode},
3324 to_branch => $library_2->{branchcode},
3325 barcode => $item->barcode,
3327 my $hold = Koha::Holds->find( $reserve_id );
3328 is( $hold->found, 'T', 'Hold is in transit' );
3330 #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
3331 my ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3332 is( $frombranch, $library_1->{branchcode}, 'The transfer is generated from the correct library');
3333 is( $tobranch, $library_2->{branchcode}, 'The transfer is generated to the correct library');
3334 my $itemcheck = Koha::Items->find($item->itemnumber);
3335 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Items holding branch is the transfers origination branch before it is marked as lost' );
3337 #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
3338 $item->itemlost(1)->store;
3339 LostItem( $item->itemnumber, 'test', 1 );
3340 ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3341 is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
3342 $itemcheck = Koha::Items->find($item->itemnumber);
3343 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
3347 subtest 'CanBookBeIssued | is_overdue' => sub {
3350 # Set a simple circ policy
3351 Koha::CirculationRules->set_rules(
3353 categorycode => undef,
3354 branchcode => undef,
3358 reservesallowed => 25,
3360 lengthunit => 'days',
3361 renewalsallowed => 1,
3363 norenewalbefore => undef,
3371 my $now = dt_from_string;
3372 my $five_days_go = output_pref({ dt => $now->clone->add( days => 5 ), dateonly => 1});
3373 my $ten_days_go = output_pref({ dt => $now->clone->add( days => 10), dateonly => 1 });
3374 my $library = $builder->build( { source => 'Branch' } );
3375 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
3377 my $item = $builder->build_sample_item(
3379 library => $library->{branchcode},
3383 my $issue = AddIssue( $patron->unblessed, $item->barcode, $five_days_go ); # date due was 10d ago
3384 my $actualissue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
3385 is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
3386 my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->barcode,$ten_days_go, undef, undef, undef);
3387 is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
3388 is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
3391 subtest 'ItemsDeniedRenewal preference' => sub {
3394 C4::Context->set_preference('ItemsDeniedRenewal','');
3396 my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
3397 Koha::CirculationRules->set_rules(
3399 categorycode => '*',
3401 branchcode => $idr_lib->branchcode,
3403 reservesallowed => 25,
3405 lengthunit => 'days',
3406 renewalsallowed => 10,
3408 norenewalbefore => undef,
3416 my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
3417 homebranch => $idr_lib->branchcode,
3421 itemcallnumber => undef,
3425 my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
3426 homebranch => $idr_lib->branchcode,
3429 location => 'NOPROC'
3433 my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
3434 branchcode => $idr_lib->branchcode,
3437 my $future = dt_from_string->add( days => 1 );
3438 my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3439 returndate => undef,
3442 borrowernumber => $idr_borrower->borrowernumber,
3443 itemnumber => $deny_book->itemnumber,
3444 onsite_checkout => 0,
3445 date_due => $future,
3448 my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3449 returndate => undef,
3452 borrowernumber => $idr_borrower->borrowernumber,
3453 itemnumber => $allow_book->itemnumber,
3454 onsite_checkout => 0,
3455 date_due => $future,
3461 my ( $idr_mayrenew, $idr_error ) =
3462 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3463 is( $idr_mayrenew, 1, 'Renewal allowed when no rules' );
3464 is( $idr_error, undef, 'Renewal allowed when no rules' );
3466 $idr_rules="withdrawn: [1]";
3468 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3469 ( $idr_mayrenew, $idr_error ) =
3470 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3471 is( $idr_mayrenew, 0, 'Renewal blocked when 1 rules (withdrawn)' );
3472 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 1 rule (withdrawn)' );
3473 ( $idr_mayrenew, $idr_error ) =
3474 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3475 is( $idr_mayrenew, 1, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3476 is( $idr_error, undef, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3478 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
3480 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3481 ( $idr_mayrenew, $idr_error ) =
3482 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3483 is( $idr_mayrenew, 0, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
3484 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 2 rules matched (withdrawn,itype)' );
3485 ( $idr_mayrenew, $idr_error ) =
3486 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3487 is( $idr_mayrenew, 1, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3488 is( $idr_error, undef, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3490 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
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, 0, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
3496 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 3 rules matched (withdrawn,itype, location)' );
3497 ( $idr_mayrenew, $idr_error ) =
3498 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3499 is( $idr_mayrenew, 1, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3500 is( $idr_error, undef, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3502 $idr_rules="itemcallnumber: [NULL]";
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 undef when NULL in pref' );
3507 $idr_rules="itemcallnumber: ['']";
3508 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3509 ( $idr_mayrenew, $idr_error ) =
3510 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3511 is( $idr_mayrenew, 1, 'Renewal not blocked for undef when "" in pref' );
3513 $idr_rules="itemnotes: [NULL]";
3514 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3515 ( $idr_mayrenew, $idr_error ) =
3516 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3517 is( $idr_mayrenew, 1, 'Renewal not blocked for "" when NULL in pref' );
3518 $idr_rules="itemnotes: ['']";
3519 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3520 ( $idr_mayrenew, $idr_error ) =
3521 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3522 is( $idr_mayrenew, 0, 'Renewal blocked for empty string when "" in pref' );
3525 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
3528 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3529 my $library = $builder->build( { source => 'Branch' } );
3530 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3532 my $item = $builder->build_sample_item(
3534 library => $library->{branchcode},
3538 my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3539 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3540 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3543 subtest 'CanBookBeIssued | notforloan' => sub {
3546 t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
3548 my $library = $builder->build( { source => 'Branch' } );
3549 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3551 my $itemtype = $builder->build(
3553 source => 'Itemtype',
3554 value => { notforloan => undef, }
3557 my $item = $builder->build_sample_item(
3559 library => $library->{branchcode},
3560 itype => $itemtype->{itemtype},
3563 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3565 my ( $issuingimpossible, $needsconfirmation );
3568 subtest 'item-level_itypes = 1' => sub {
3571 t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
3572 # Is for loan at item type and item level
3573 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3574 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3575 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3577 # not for loan at item type level
3578 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3579 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3580 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3583 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3584 'Item can not be issued, not for loan at item type level'
3587 # not for loan at item level
3588 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3589 $item->notforloan( 1 )->store;
3590 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3591 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3594 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3595 'Item can not be issued, not for loan at item type level'
3599 subtest 'item-level_itypes = 0' => sub {
3602 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3604 # We set another itemtype for biblioitem
3605 my $itemtype = $builder->build(
3607 source => 'Itemtype',
3608 value => { notforloan => undef, }
3612 # for loan at item type and item level
3613 $item->notforloan(0)->store;
3614 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3615 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3616 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3617 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3619 # not for loan at item type level
3620 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3621 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3622 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3625 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3626 'Item can not be issued, not for loan at item type level'
3629 # not for loan at item level
3630 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3631 $item->notforloan( 1 )->store;
3632 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3633 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3636 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3637 'Item can not be issued, not for loan at item type level'
3641 # TODO test with AllowNotForLoanOverride = 1
3644 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
3647 t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
3648 my $item = $builder->build_sample_item(
3650 onloan => '2018-01-01',
3654 AddReturn( $item->barcode, $item->homebranch );
3655 $item->discard_changes; # refresh
3656 is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
3660 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
3665 t::lib::Mocks::mock_preference('item-level_itypes', 1);
3667 my $issuing_charges = 15;
3668 my $title = 'A title';
3669 my $author = 'Author, An';
3670 my $barcode = 'WHATARETHEODDS';
3672 my $circ = Test::MockModule->new('C4::Circulation');
3674 'GetIssuingCharges',
3676 return $issuing_charges;
3680 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3681 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
3682 my $patron = $builder->build_object({
3683 class => 'Koha::Patrons',
3684 value => { branchcode => $library->id }
3687 my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
3688 my $item_id = Koha::Item->new(
3690 biblionumber => $biblio->biblionumber,
3691 homebranch => $library->id,
3692 holdingbranch => $library->id,
3693 barcode => $barcode,
3694 replacementprice => 23.00,
3695 itype => $itemtype->id
3697 )->store->itemnumber;
3698 my $item = Koha::Items->find( $item_id );
3700 my $context = Test::MockModule->new('C4::Context');
3701 $context->mock( userenv => { branch => $library->id } );
3703 # Check the item out
3704 AddIssue( $patron->unblessed, $item->barcode );
3705 t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
3706 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
3707 my %params_renewal = (
3708 timestamp => { -like => $date . "%" },
3709 module => "CIRCULATION",
3710 action => "RENEWAL",
3712 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
3713 AddRenewal( $patron->id, $item->id, $library->id );
3714 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3715 is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
3717 my $checkouts = $patron->checkouts;
3718 # The following will fail if run on 00:00:00
3719 unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
3721 my $lines = Koha::Account::Lines->search({
3722 borrowernumber => $patron->id,
3723 itemnumber => $item->id
3726 is( $lines->count, 2 );
3728 my $line = $lines->next;
3729 is( $line->debit_type_code, 'RENT', 'The issue of item with issuing charge generates an accountline of the correct type' );
3730 is( $line->branchcode, $library->id, 'AddIssuingCharge correctly sets branchcode' );
3731 is( $line->description, '', 'AddIssue does not set a hardcoded description for the accountline' );
3733 $line = $lines->next;
3734 is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
3735 is( $line->branchcode, $library->id, 'AddRenewal correctly sets branchcode' );
3736 is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
3738 t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
3740 $context = Test::MockModule->new('C4::Context');
3741 $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
3743 my $now = dt_from_string;
3744 $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
3745 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
3746 my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
3747 $sth->execute($item->id, $library->id);
3748 my ($old_stats_size) = $sth->fetchrow_array;
3749 AddRenewal( $patron->id, $item->id, $library->id );
3750 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3751 $sth->execute($item->id, $library->id);
3752 my ($new_stats_size) = $sth->fetchrow_array;
3753 is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
3754 is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
3756 AddReturn( $item->id, $library->id, undef, $date );
3757 AddIssue( $patron->unblessed, $item->barcode, $now );
3758 AddRenewal( $patron->id, $item->id, $library->id, undef, undef, 1 );
3759 my $lines_skipped = Koha::Account::Lines->search({
3760 borrowernumber => $patron->id,
3761 itemnumber => $item->id
3763 is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
3767 subtest 'ProcessOfflinePayment() tests' => sub {
3774 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
3775 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3776 my $result = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
3778 is( $result, 'Success.', 'The right string is returned' );
3780 my $lines = $patron->account->lines;
3781 is( $lines->count, 1, 'line created correctly');
3783 my $line = $lines->next;
3784 is( $line->amount+0, $amount * -1, 'amount picked from params' );
3785 is( $line->branchcode, $library->id, 'branchcode set correctly' );
3789 subtest 'Incremented fee tests' => sub {
3792 my $dt = dt_from_string();
3793 Time::Fake->offset( $dt->epoch );
3795 t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
3798 $builder->build_object( { class => 'Koha::Libraries' } )->store;
3800 my $module = Test::MockModule->new('C4::Context');
3801 $module->mock( 'userenv', sub { { branch => $library->id } } );
3803 my $patron = $builder->build_object(
3805 class => 'Koha::Patrons',
3806 value => { categorycode => $patron_category->{categorycode} }
3810 my $itemtype = $builder->build_object(
3812 class => 'Koha::ItemTypes',
3814 notforloan => undef,
3816 rentalcharge_daily => 1,
3817 rentalcharge_daily_calendar => 0
3822 my $item = $builder->build_sample_item(
3824 library => $library->{branchcode},
3825 itype => $itemtype->id,
3829 is( $itemtype->rentalcharge_daily+0,
3830 1, 'Daily rental charge stored and retreived correctly' );
3831 is( $item->effective_itemtype, $itemtype->id,
3832 "Itemtype set correctly for item" );
3834 my $now = dt_from_string;
3835 my $dt_from = $now->clone;
3836 my $dt_to = $now->clone->add( days => 7 );
3837 my $dt_to_renew = $now->clone->add( days => 13 );
3841 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3842 my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3843 is( $accountline->amount+0, 7,
3844 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
3846 $accountline->delete();
3847 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3848 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3849 is( $accountline->amount+0, 6,
3850 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
3852 $accountline->delete();
3855 t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
3856 $itemtype->rentalcharge_daily_calendar(1)->store();
3858 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3859 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3860 is( $accountline->amount+0, 7,
3861 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
3863 $accountline->delete();
3864 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3865 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3866 is( $accountline->amount+0, 6,
3867 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
3869 $accountline->delete();
3872 my $calendar = C4::Calendar->new( branchcode => $library->id );
3873 # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
3875 ( $dt_from->day_of_week == 6 ) ? 0
3876 : ( $dt_from->day_of_week == 7 ) ? 1
3877 : $dt_from->day_of_week + 1;
3878 my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
3879 $calendar->insert_week_day_holiday(
3880 weekday => $closed_day,
3881 title => 'Test holiday',
3882 description => 'Test holiday'
3885 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3886 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3887 is( $accountline->amount+0, 6,
3888 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
3890 $accountline->delete();
3891 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3892 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3893 is( $accountline->amount+0, 5,
3894 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
3896 $accountline->delete();
3899 $itemtype->rentalcharge(2)->store;
3900 is( $itemtype->rentalcharge+0, 2,
3901 'Rental charge updated and retreived correctly' );
3903 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3905 Koha::Account::Lines->search( { itemnumber => $item->id } );
3906 is( $accountlines->count, '2',
3907 "Fixed charge and accrued charge recorded distinctly" );
3908 $accountlines->delete();
3909 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3910 $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
3911 is( $accountlines->count, '2',
3912 "Fixed charge and accrued charge recorded distinctly, for renewal" );
3913 $accountlines->delete();
3915 $itemtype->rentalcharge(0)->store;
3916 is( $itemtype->rentalcharge+0, 0,
3917 'Rental charge reset and retreived correctly' );
3920 Koha::CirculationRules->set_rule(
3922 categorycode => $patron->categorycode,
3923 itemtype => $itemtype->id,
3924 branchcode => $library->id,
3925 rule_name => 'lengthunit',
3926 rule_value => 'hours',
3930 $itemtype->rentalcharge_hourly('0.25')->store();
3931 is( $itemtype->rentalcharge_hourly,
3932 '0.25', 'Hourly rental charge stored and retreived correctly' );
3934 $dt_to = $now->clone->add( hours => 168 );
3935 $dt_to_renew = $now->clone->add( hours => 312 );
3937 $itemtype->rentalcharge_hourly_calendar(0)->store();
3939 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3940 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3941 is( $accountline->amount + 0, 42,
3942 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)" );
3943 $accountline->delete();
3944 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3945 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3946 is( $accountline->amount + 0, 36,
3947 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)" );
3948 $accountline->delete();
3951 $itemtype->rentalcharge_hourly_calendar(1)->store();
3953 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3954 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3955 is( $accountline->amount + 0, 36,
3956 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)" );
3957 $accountline->delete();
3958 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3959 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3960 is( $accountline->amount + 0, 30,
3961 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u" );
3962 $accountline->delete();
3965 $calendar->delete_holiday( weekday => $closed_day );
3967 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3968 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3969 is( $accountline->amount + 0, 42,
3970 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u" );
3971 $accountline->delete();
3972 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3973 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3974 is( $accountline->amount + 0, 36,
3975 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)" );
3976 $accountline->delete();
3981 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
3984 t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
3985 t::lib::Mocks::mock_preference('item-level_itypes', 1);
3988 $builder->build_object( { class => 'Koha::Libraries' } )->store;
3989 my $patron = $builder->build_object(
3991 class => 'Koha::Patrons',
3992 value => { categorycode => $patron_category->{categorycode} }
3996 my $itemtype = $builder->build_object(
3998 class => 'Koha::ItemTypes',
4002 rentalcharge_daily => 0
4007 my $item = $builder->build_sample_item(
4009 library => $library->id,
4013 itype => $itemtype->id,
4017 my ( $issuingimpossible, $needsconfirmation );
4018 my $dt_from = dt_from_string();
4019 my $dt_due = $dt_from->clone->add( days => 3 );
4021 $itemtype->rentalcharge(1)->store;
4022 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4023 is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
4024 $itemtype->rentalcharge('0')->store;
4025 $itemtype->rentalcharge_daily(1)->store;
4026 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4027 is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
4028 $itemtype->rentalcharge_daily('0')->store;
4031 subtest 'CanBookBeIssued & CircConfirmItemParts' => sub {
4034 t::lib::Mocks::mock_preference('CircConfirmItemParts', 1);
4036 my $patron = $builder->build_object(
4038 class => 'Koha::Patrons',
4039 value => { categorycode => $patron_category->{categorycode} }
4043 my $item = $builder->build_sample_item(
4045 materials => 'includes DVD',
4049 my $dt_due = dt_from_string->add( days => 3 );
4051 my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4052 is_deeply( $needsconfirmation, { ADDITIONAL_MATERIALS => 'includes DVD' }, 'Item needs confirmation of additional parts' );
4055 subtest 'Do not return on renewal (LOST charge)' => sub {
4058 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
4059 my $library = $builder->build_object( { class => "Koha::Libraries" } );
4060 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
4061 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
4063 my $biblio = $builder->build_sample_biblio;
4065 my $item = $builder->build_sample_item(
4067 biblionumber => $biblio->biblionumber,
4068 library => $library->branchcode,
4069 replacementprice => 99.00,
4074 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
4075 AddIssue( $patron->unblessed, $item->barcode );
4077 my $accountline = Koha::Account::Line->new(
4079 borrowernumber => $patron->borrowernumber,
4080 debit_type_code => 'LOST',
4082 itemnumber => $item->itemnumber,
4084 amountoutstanding => 12,
4085 interface => 'something',
4089 # AddRenewal doesn't call _FixAccountForLostAndFound
4090 AddIssue( $patron->unblessed, $item->barcode );
4092 is( $patron->checkouts->count, 1,
4093 'Renewal should not return the item even if a LOST payment has been made earlier'
4097 subtest 'Filling a hold should cancel existing transfer' => sub {
4100 t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
4102 my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
4103 my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
4104 my $patron = $builder->build_object(
4106 class => 'Koha::Patrons',
4108 categorycode => $patron_category->{categorycode},
4109 branchcode => $libraryA->branchcode,
4114 my $item = $builder->build_sample_item({
4115 homebranch => $libraryB->branchcode,
4118 my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4119 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
4121 branchcode => $libraryA->branchcode,
4122 borrowernumber => $patron->borrowernumber,
4123 biblionumber => $item->biblionumber,
4124 itemnumber => $item->itemnumber
4126 my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
4127 is( $reserves->count, 1, "Reserve is placed");
4128 ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4129 my $reserve = $reserves->next;
4130 ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
4131 $reserve->discard_changes;
4132 ok( $reserve->found eq 'W', "Reserve is marked waiting" );
4133 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
4136 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddReturn' => sub {
4140 t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
4141 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4142 my $patron = $builder->build_object(
4144 class => 'Koha::Patrons',
4145 value => { categorycode => $patron_category->{categorycode} }
4149 my $biblionumber = $builder->build_sample_biblio(
4151 branchcode => $library->branchcode,
4155 # And the circulation rule
4156 Koha::CirculationRules->search->delete;
4157 Koha::CirculationRules->set_rules(
4159 categorycode => undef,
4161 branchcode => undef,
4164 lengthunit => 'days',
4170 source => 'CirculationRule',
4172 branchcode => undef,
4173 categorycode => undef,
4175 rule_name => 'lostreturn',
4176 rule_value => 'refund'
4181 subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
4184 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4185 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
4187 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4189 my $item = $builder->build_sample_item(
4191 biblionumber => $biblionumber,
4192 library => $library->branchcode,
4193 replacementprice => '42',
4196 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4197 LostItem( $item->itemnumber, 'cli', 0 );
4198 $item->_result->itemlost(1);
4199 $item->_result->itemlost_on( $lost_on );
4200 $item->_result->update();
4202 my $a = Koha::Account::Lines->search(
4204 itemnumber => $item->id,
4205 borrowernumber => $patron->borrowernumber
4208 ok( $a, "Found accountline for lost fee" );
4209 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4210 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4211 $a = $a->get_from_storage;
4212 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4216 subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
4219 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4220 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4222 my $lost_on = dt_from_string->subtract( days => 6 )->date;
4224 my $item = $builder->build_sample_item(
4226 biblionumber => $biblionumber,
4227 library => $library->branchcode,
4228 replacementprice => '42',
4231 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4232 LostItem( $item->itemnumber, 'cli', 0 );
4233 $item->_result->itemlost(1);
4234 $item->_result->itemlost_on( $lost_on );
4235 $item->_result->update();
4237 my $a = Koha::Account::Lines->search(
4239 itemnumber => $item->id,
4240 borrowernumber => $patron->borrowernumber
4243 ok( $a, "Found accountline for lost fee" );
4244 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4245 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4246 $a = $a->get_from_storage;
4247 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4251 subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
4254 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4255 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4257 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4259 my $item = $builder->build_sample_item(
4261 biblionumber => $biblionumber,
4262 library => $library->branchcode,
4263 replacementprice => '42',
4266 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4267 LostItem( $item->itemnumber, 'cli', 0 );
4268 $item->_result->itemlost(1);
4269 $item->_result->itemlost_on( $lost_on );
4270 $item->_result->update();
4272 my $a = Koha::Account::Lines->search(
4274 itemnumber => $item->id,
4275 borrowernumber => $patron->borrowernumber
4278 ok( $a, "Found accountline for lost fee" );
4279 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4280 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4281 $a = $a->get_from_storage;
4282 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4286 subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
4289 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4290 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4292 my $lost_on = dt_from_string->subtract( days => 8 )->date;
4294 my $item = $builder->build_sample_item(
4296 biblionumber => $biblionumber,
4297 library => $library->branchcode,
4298 replacementprice => '42',
4301 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4302 LostItem( $item->itemnumber, 'cli', 0 );
4303 $item->_result->itemlost(1);
4304 $item->_result->itemlost_on( $lost_on );
4305 $item->_result->update();
4307 my $a = Koha::Account::Lines->search(
4309 itemnumber => $item->id,
4310 borrowernumber => $patron->borrowernumber
4314 ok( $a, "Found accountline for lost fee" );
4315 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4316 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4317 $a = $a->get_from_storage;
4318 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4323 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddIssue' => sub {
4327 t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
4328 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4329 my $patron = $builder->build_object(
4331 class => 'Koha::Patrons',
4332 value => { categorycode => $patron_category->{categorycode} }
4335 my $patron2 = $builder->build_object(
4337 class => 'Koha::Patrons',
4338 value => { categorycode => $patron_category->{categorycode} }
4342 my $biblionumber = $builder->build_sample_biblio(
4344 branchcode => $library->branchcode,
4348 # And the circulation rule
4349 Koha::CirculationRules->search->delete;
4350 Koha::CirculationRules->set_rules(
4352 categorycode => undef,
4354 branchcode => undef,
4357 lengthunit => 'days',
4363 source => 'CirculationRule',
4365 branchcode => undef,
4366 categorycode => undef,
4368 rule_name => 'lostreturn',
4369 rule_value => 'refund'
4374 subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
4377 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4378 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
4380 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4382 my $item = $builder->build_sample_item(
4384 biblionumber => $biblionumber,
4385 library => $library->branchcode,
4386 replacementprice => '42',
4389 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4390 LostItem( $item->itemnumber, 'cli', 0 );
4391 $item->_result->itemlost(1);
4392 $item->_result->itemlost_on( $lost_on );
4393 $item->_result->update();
4395 my $a = Koha::Account::Lines->search(
4397 itemnumber => $item->id,
4398 borrowernumber => $patron->borrowernumber
4401 ok( $a, "Found accountline for lost fee" );
4402 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4403 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4404 $a = $a->get_from_storage;
4405 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4410 subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
4413 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4414 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4416 my $lost_on = dt_from_string->subtract( days => 6 )->date;
4418 my $item = $builder->build_sample_item(
4420 biblionumber => $biblionumber,
4421 library => $library->branchcode,
4422 replacementprice => '42',
4425 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4426 LostItem( $item->itemnumber, 'cli', 0 );
4427 $item->_result->itemlost(1);
4428 $item->_result->itemlost_on( $lost_on );
4429 $item->_result->update();
4431 my $a = Koha::Account::Lines->search(
4433 itemnumber => $item->id,
4434 borrowernumber => $patron->borrowernumber
4437 ok( $a, "Found accountline for lost fee" );
4438 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4439 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4440 $a = $a->get_from_storage;
4441 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4445 subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
4448 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4449 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4451 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4453 my $item = $builder->build_sample_item(
4455 biblionumber => $biblionumber,
4456 library => $library->branchcode,
4457 replacementprice => '42',
4460 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4461 LostItem( $item->itemnumber, 'cli', 0 );
4462 $item->_result->itemlost(1);
4463 $item->_result->itemlost_on( $lost_on );
4464 $item->_result->update();
4466 my $a = Koha::Account::Lines->search(
4468 itemnumber => $item->id,
4469 borrowernumber => $patron->borrowernumber
4472 ok( $a, "Found accountline for lost fee" );
4473 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4474 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4475 $a = $a->get_from_storage;
4476 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4480 subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
4483 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4484 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4486 my $lost_on = dt_from_string->subtract( days => 8 )->date;
4488 my $item = $builder->build_sample_item(
4490 biblionumber => $biblionumber,
4491 library => $library->branchcode,
4492 replacementprice => '42',
4495 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4496 LostItem( $item->itemnumber, 'cli', 0 );
4497 $item->_result->itemlost(1);
4498 $item->_result->itemlost_on( $lost_on );
4499 $item->_result->update();
4501 my $a = Koha::Account::Lines->search(
4503 itemnumber => $item->id,
4504 borrowernumber => $patron->borrowernumber
4508 ok( $a, "Found accountline for lost fee" );
4509 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4510 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4511 $a = $a->get_from_storage;
4512 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4517 subtest 'transferbook tests' => sub {
4521 { C4::Circulation::transferbook({}); }
4522 'Koha::Exceptions::MissingParameter',
4523 'Koha::Patron->store raises an exception on missing params';
4526 { C4::Circulation::transferbook({to_branch=>'anything'}); }
4527 'Koha::Exceptions::MissingParameter',
4528 'Koha::Patron->store raises an exception on missing params';
4531 { C4::Circulation::transferbook({from_branch=>'anything'}); }
4532 'Koha::Exceptions::MissingParameter',
4533 'Koha::Patron->store raises an exception on missing params';
4535 my ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here'});
4536 is( $doreturn, 0, "No return without barcode");
4537 ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
4538 is( $messages->{BadBarcode}, undef, "No barcode passed means undef BadBarcode" );
4540 ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here',barcode=>'BadBarcode'});
4541 is( $doreturn, 0, "No return without barcode");
4542 ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
4543 is( $messages->{BadBarcode}, 'BadBarcode', "No barcode passed means undef BadBarcode" );
4547 subtest 'Checkout should correctly terminate a transfer' => sub {
4550 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
4551 my $patron_1 = $builder->build_object(
4553 class => 'Koha::Patrons',
4554 value => { branchcode => $library_1->branchcode }
4557 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
4558 my $patron_2 = $builder->build_object(
4560 class => 'Koha::Patrons',
4561 value => { branchcode => $library_2->branchcode }
4565 my $item = $builder->build_sample_item(
4567 library => $library_1->branchcode,
4571 t::lib::Mocks::mock_userenv( { branchcode => $library_1->branchcode } );
4572 my $reserve_id = AddReserve(
4574 branchcode => $library_2->branchcode,
4575 borrowernumber => $patron_2->borrowernumber,
4576 biblionumber => $item->biblionumber,
4577 itemnumber => $item->itemnumber,
4582 my $do_transfer = 1;
4583 ModItemTransfer( $item->itemnumber, $library_1->branchcode,
4584 $library_2->branchcode );
4585 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4586 GetOtherReserves( $item->itemnumber )
4587 ; # To put the Reason, it's what does returns.pl...
4588 my $hold = Koha::Holds->find($reserve_id);
4589 is( $hold->found, 'T', 'Hold is in transit' );
4590 my $transfer = $item->get_transfer;
4591 is( $transfer->frombranch, $library_1->branchcode );
4592 is( $transfer->tobranch, $library_2->branchcode );
4593 is( $transfer->reason, 'Reserve' );
4595 t::lib::Mocks::mock_userenv( { branchcode => $library_2->branchcode } );
4596 AddIssue( $patron_1->unblessed, $item->barcode );
4597 $transfer = $transfer->get_from_storage;
4598 isnt( $transfer->datearrived, undef );
4599 $hold = $hold->get_from_storage;
4600 is( $hold->found, undef, 'Hold is waiting' );
4601 is( $hold->priority, 1, );
4604 $schema->storage->txn_rollback;
4605 C4::Context->clear_syspref_cache();
4606 $branches = Koha::Libraries->search();
4607 for my $branch ( $branches->next ) {
4608 my $key = $branch->branchcode . "_holidays";
4609 $cache->clear_from_cache($key);