Bug 26922: Regression tests
[koha.git] / t / db_dependent / Circulation.t
blobf29c175aefad135aecc5150f1cfdbc9d3fafc290
1 #!/usr/bin/perl
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>.
18 use Modern::Perl;
19 use utf8;
21 use Test::More tests => 51;
22 use Test::Exception;
23 use Test::MockModule;
24 use Test::Deep qw( cmp_deeply );
25 use Test::Warn;
27 use Data::Dumper;
28 use DateTime;
29 use Time::Fake;
30 use POSIX qw( floor );
31 use t::lib::Mocks;
32 use t::lib::TestBuilder;
34 use C4::Accounts;
35 use C4::Calendar;
36 use C4::Circulation;
37 use C4::Biblio;
38 use C4::Items;
39 use C4::Log;
40 use C4::Reserves;
41 use C4::Overdues qw(UpdateFine CalcFine);
42 use Koha::DateUtils;
43 use Koha::Database;
44 use Koha::Items;
45 use Koha::Item::Transfers;
46 use Koha::Checkouts;
47 use Koha::Patrons;
48 use Koha::Holds;
49 use Koha::CirculationRules;
50 use Koha::Subscriptions;
51 use Koha::Account::Lines;
52 use Koha::Account::Offsets;
53 use Koha::ActionLogs;
55 sub set_userenv {
56 my ( $library ) = @_;
57 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
60 sub str {
61 my ( $error, $question, $alert ) = @_;
62 my $s;
63 $s = %$error ? ' (error: ' . join( ' ', keys %$error ) . ')' : '';
64 $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
65 $s .= %$alert ? ' (alert: ' . join( ' ', keys %$alert ) . ')' : '';
66 return $s;
69 sub test_debarment_on_checkout {
70 my ($params) = @_;
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,
81 dateformat => 'sql',
82 dateonly => 1,
85 my @caller = caller;
86 my $line_number = $caller[2];
87 AddIssue( $patron, $item->barcode, $due_date );
89 my ( undef, $message ) = AddReturn( $item->barcode, $library->{branchcode}, undef, $return_date );
90 is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
91 or diag('AddReturn returned message ' . Dumper $message );
92 my $debarments = Koha::Patron::Debarments::GetDebarments(
93 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
94 is( scalar(@$debarments), 1, 'Test at line ' . $line_number );
96 is( $debarments->[0]->{expiration},
97 $expected_expiration_date, 'Test at line ' . $line_number );
98 Koha::Patron::Debarments::DelUniqueDebarment(
99 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
102 my $schema = Koha::Database->schema;
103 $schema->storage->txn_begin;
104 my $builder = t::lib::TestBuilder->new;
105 my $dbh = C4::Context->dbh;
107 # Prevent random failures by mocking ->now
108 my $now_value = dt_from_string;
109 my $mocked_datetime = Test::MockModule->new('DateTime');
110 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
112 my $cache = Koha::Caches->get_instance();
113 $dbh->do(q|DELETE FROM special_holidays|);
114 $dbh->do(q|DELETE FROM repeatable_holidays|);
115 my $branches = Koha::Libraries->search();
116 for my $branch ( $branches->next ) {
117 my $key = $branch->branchcode . "_holidays";
118 $cache->clear_from_cache($key);
121 # Start with a clean slate
122 $dbh->do('DELETE FROM issues');
123 $dbh->do('DELETE FROM borrowers');
125 # Disable recording of the staff who checked out an item until we're ready for it
126 t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 0);
128 my $module = Test::MockModule->new('C4::Context');
130 my $library = $builder->build({
131 source => 'Branch',
133 my $library2 = $builder->build({
134 source => 'Branch',
136 my $itemtype = $builder->build(
138 source => 'Itemtype',
139 value => {
140 notforloan => undef,
141 rentalcharge => 0,
142 rentalcharge_daily => 0,
143 defaultreplacecost => undef,
144 processfee => undef
147 )->{itemtype};
148 my $patron_category = $builder->build(
150 source => 'Category',
151 value => {
152 category_type => 'P',
153 enrolmentfee => 0,
154 BlockExpiredPatronOpacActions => -1, # Pick the pref value
159 my $CircControl = C4::Context->preference('CircControl');
160 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
162 my $item = {
163 homebranch => $library2->{branchcode},
164 holdingbranch => $library2->{branchcode}
167 my $borrower = {
168 branchcode => $library2->{branchcode}
171 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
173 # No userenv, PickupLibrary
174 t::lib::Mocks::mock_preference('IndependentBranches', '0');
175 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
177 C4::Context->preference('CircControl'),
178 'PickupLibrary',
179 'CircControl changed to PickupLibrary'
182 C4::Circulation::_GetCircControlBranch($item, $borrower),
183 $item->{$HomeOrHoldingBranch},
184 '_GetCircControlBranch returned item branch (no userenv defined)'
187 # No userenv, PatronLibrary
188 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
190 C4::Context->preference('CircControl'),
191 'PatronLibrary',
192 'CircControl changed to PatronLibrary'
195 C4::Circulation::_GetCircControlBranch($item, $borrower),
196 $borrower->{branchcode},
197 '_GetCircControlBranch returned borrower branch'
200 # No userenv, ItemHomeLibrary
201 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
203 C4::Context->preference('CircControl'),
204 'ItemHomeLibrary',
205 'CircControl changed to ItemHomeLibrary'
208 $item->{$HomeOrHoldingBranch},
209 C4::Circulation::_GetCircControlBranch($item, $borrower),
210 '_GetCircControlBranch returned item branch'
213 # Now, set a userenv
214 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
215 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
217 # Userenv set, PickupLibrary
218 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
220 C4::Context->preference('CircControl'),
221 'PickupLibrary',
222 'CircControl changed to PickupLibrary'
225 C4::Circulation::_GetCircControlBranch($item, $borrower),
226 $library2->{branchcode},
227 '_GetCircControlBranch returned current branch'
230 # Userenv set, PatronLibrary
231 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
233 C4::Context->preference('CircControl'),
234 'PatronLibrary',
235 'CircControl changed to PatronLibrary'
238 C4::Circulation::_GetCircControlBranch($item, $borrower),
239 $borrower->{branchcode},
240 '_GetCircControlBranch returned borrower branch'
243 # Userenv set, ItemHomeLibrary
244 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
246 C4::Context->preference('CircControl'),
247 'ItemHomeLibrary',
248 'CircControl changed to ItemHomeLibrary'
251 C4::Circulation::_GetCircControlBranch($item, $borrower),
252 $item->{$HomeOrHoldingBranch},
253 '_GetCircControlBranch returned item branch'
256 # Reset initial configuration
257 t::lib::Mocks::mock_preference('CircControl', $CircControl);
259 C4::Context->preference('CircControl'),
260 $CircControl,
261 'CircControl reset to its initial value'
264 # Set a simple circ policy
265 $dbh->do('DELETE FROM circulation_rules');
266 Koha::CirculationRules->set_rules(
268 categorycode => undef,
269 branchcode => undef,
270 itemtype => undef,
271 rules => {
272 reservesallowed => 25,
273 issuelength => 14,
274 lengthunit => 'days',
275 renewalsallowed => 1,
276 renewalperiod => 7,
277 norenewalbefore => undef,
278 auto_renew => 0,
279 fine => .10,
280 chargeperiod => 1,
285 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
286 subtest "CanBookBeRenewed tests" => sub {
287 plan tests => 89;
289 C4::Context->set_preference('ItemsDeniedRenewal','');
290 # Generate test biblio
291 my $biblio = $builder->build_sample_biblio();
293 my $branch = $library2->{branchcode};
295 my $item_1 = $builder->build_sample_item(
297 biblionumber => $biblio->biblionumber,
298 library => $branch,
299 replacementprice => 12.00,
300 itype => $itemtype
303 $reused_itemnumber_1 = $item_1->itemnumber;
305 my $item_2 = $builder->build_sample_item(
307 biblionumber => $biblio->biblionumber,
308 library => $branch,
309 replacementprice => 23.00,
310 itype => $itemtype
313 $reused_itemnumber_2 = $item_2->itemnumber;
315 my $item_3 = $builder->build_sample_item(
317 biblionumber => $biblio->biblionumber,
318 library => $branch,
319 replacementprice => 23.00,
320 itype => $itemtype
324 # Create borrowers
325 my %renewing_borrower_data = (
326 firstname => 'John',
327 surname => 'Renewal',
328 categorycode => $patron_category->{categorycode},
329 branchcode => $branch,
332 my %reserving_borrower_data = (
333 firstname => 'Katrin',
334 surname => 'Reservation',
335 categorycode => $patron_category->{categorycode},
336 branchcode => $branch,
339 my %hold_waiting_borrower_data = (
340 firstname => 'Kyle',
341 surname => 'Reservation',
342 categorycode => $patron_category->{categorycode},
343 branchcode => $branch,
346 my %restricted_borrower_data = (
347 firstname => 'Alice',
348 surname => 'Reservation',
349 categorycode => $patron_category->{categorycode},
350 debarred => '3228-01-01',
351 branchcode => $branch,
354 my %expired_borrower_data = (
355 firstname => 'Ça',
356 surname => 'Glisse',
357 categorycode => $patron_category->{categorycode},
358 branchcode => $branch,
359 dateexpiry => dt_from_string->subtract( months => 1 ),
362 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
363 my $reserving_borrowernumber = Koha::Patron->new(\%reserving_borrower_data)->store->borrowernumber;
364 my $hold_waiting_borrowernumber = Koha::Patron->new(\%hold_waiting_borrower_data)->store->borrowernumber;
365 my $restricted_borrowernumber = Koha::Patron->new(\%restricted_borrower_data)->store->borrowernumber;
366 my $expired_borrowernumber = Koha::Patron->new(\%expired_borrower_data)->store->borrowernumber;
368 my $renewing_borrower_obj = Koha::Patrons->find( $renewing_borrowernumber );
369 my $renewing_borrower = $renewing_borrower_obj->unblessed;
370 my $restricted_borrower = Koha::Patrons->find( $restricted_borrowernumber )->unblessed;
371 my $expired_borrower = Koha::Patrons->find( $expired_borrowernumber )->unblessed;
373 my $bibitems = '';
374 my $priority = '1';
375 my $resdate = undef;
376 my $expdate = undef;
377 my $notes = '';
378 my $checkitem = undef;
379 my $found = undef;
381 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
382 my $datedue = dt_from_string( $issue->date_due() );
383 is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
385 my $issue2 = AddIssue( $renewing_borrower, $item_2->barcode);
386 $datedue = dt_from_string( $issue->date_due() );
387 is (defined $issue2, 1, "Item 2 checked out, due date: " . $issue2->date_due());
390 my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
391 is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}");
393 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
394 is( $renewokay, 1, 'Can renew, no holds for this title or item');
397 # Biblio-level hold, renewal test
398 AddReserve(
400 branchcode => $branch,
401 borrowernumber => $reserving_borrowernumber,
402 biblionumber => $biblio->biblionumber,
403 priority => $priority,
404 reservation_date => $resdate,
405 expiration_date => $expdate,
406 notes => $notes,
407 itemnumber => $checkitem,
408 found => $found,
412 # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
413 Koha::CirculationRules->set_rule(
415 categorycode => undef,
416 branchcode => undef,
417 itemtype => undef,
418 rule_name => 'onshelfholds',
419 rule_value => '1',
422 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
423 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
424 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
425 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
426 is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
428 # Now let's add an item level hold, we should no longer be able to renew the item
429 my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
431 borrowernumber => $hold_waiting_borrowernumber,
432 biblionumber => $biblio->biblionumber,
433 itemnumber => $item_1->itemnumber,
434 branchcode => $branch,
435 priority => 3,
438 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
439 is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
440 $hold->delete();
442 # 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
443 # be able to renew these items
444 $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
446 borrowernumber => $hold_waiting_borrowernumber,
447 biblionumber => $biblio->biblionumber,
448 itemnumber => $item_3->itemnumber,
449 branchcode => $branch,
450 priority => 0,
451 found => 'W'
454 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
455 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
456 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
457 is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
458 t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
460 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
461 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
462 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
464 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
465 is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
466 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
468 my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
469 my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber )->unblessed;
470 AddIssue($reserving_borrower, $item_3->barcode);
471 my $reserve = $dbh->selectrow_hashref(
472 'SELECT * FROM old_reserves WHERE reserve_id = ?',
473 { Slice => {} },
474 $reserveid
476 is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
478 # Item-level hold, renewal test
479 AddReserve(
481 branchcode => $branch,
482 borrowernumber => $reserving_borrowernumber,
483 biblionumber => $biblio->biblionumber,
484 priority => $priority,
485 reservation_date => $resdate,
486 expiration_date => $expdate,
487 notes => $notes,
488 itemnumber => $item_1->itemnumber,
489 found => $found,
493 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
494 is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
495 is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
497 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber, 1);
498 is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
500 # Items can't fill hold for reasons
501 $item_1->notforloan(1)->store;
502 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
503 is( $renewokay, 1, 'Can renew, item is marked not for loan, hold does not block');
504 $item_1->set({notforloan => 0, itype => $itemtype })->store;
506 # FIXME: Add more for itemtype not for loan etc.
508 # Restricted users cannot renew when RestrictionBlockRenewing is enabled
509 my $item_5 = $builder->build_sample_item(
511 biblionumber => $biblio->biblionumber,
512 library => $branch,
513 replacementprice => 23.00,
514 itype => $itemtype,
517 my $datedue5 = AddIssue($restricted_borrower, $item_5->barcode);
518 is (defined $datedue5, 1, "Item with date due checked out, due date: $datedue5");
520 t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
521 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
522 is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
523 ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $item_5->itemnumber);
524 is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
526 # Users cannot renew an overdue item
527 my $item_6 = $builder->build_sample_item(
529 biblionumber => $biblio->biblionumber,
530 library => $branch,
531 replacementprice => 23.00,
532 itype => $itemtype,
536 my $item_7 = $builder->build_sample_item(
538 biblionumber => $biblio->biblionumber,
539 library => $branch,
540 replacementprice => 23.00,
541 itype => $itemtype,
545 my $datedue6 = AddIssue( $renewing_borrower, $item_6->barcode);
546 is (defined $datedue6, 1, "Item 2 checked out, due date: ".$datedue6->date_due);
548 my $now = dt_from_string();
549 my $five_weeks = DateTime::Duration->new(weeks => 5);
550 my $five_weeks_ago = $now - $five_weeks;
551 t::lib::Mocks::mock_preference('finesMode', 'production');
553 my $passeddatedue1 = AddIssue($renewing_borrower, $item_7->barcode, $five_weeks_ago);
554 is (defined $passeddatedue1, 1, "Item with passed date due checked out, due date: " . $passeddatedue1->date_due);
556 my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower->{categorycode}, $branch, $five_weeks_ago, $now );
557 C4::Overdues::UpdateFine(
559 issue_id => $passeddatedue1->id(),
560 itemnumber => $item_7->itemnumber,
561 borrowernumber => $renewing_borrower->{borrowernumber},
562 amount => $fine,
563 due => Koha::DateUtils::output_pref($five_weeks_ago)
567 t::lib::Mocks::mock_preference('RenewalLog', 0);
568 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
569 my %params_renewal = (
570 timestamp => { -like => $date . "%" },
571 module => "CIRCULATION",
572 action => "RENEWAL",
574 my %params_issue = (
575 timestamp => { -like => $date . "%" },
576 module => "CIRCULATION",
577 action => "ISSUE"
579 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );
580 my $dt = dt_from_string();
581 Time::Fake->offset( $dt->epoch );
582 my $datedue1 = AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
583 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
584 is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
585 isnt (DateTime->compare($datedue1, $dt), 0, "AddRenewal returned a good duedate");
586 Time::Fake->reset;
588 t::lib::Mocks::mock_preference('RenewalLog', 1);
589 $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
590 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
591 AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
592 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
593 is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
595 my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
596 is( $fines->count, 2, 'AddRenewal left both fines' );
597 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
598 isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
599 $fines->delete();
602 my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
603 my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
604 AddIssue( $renewing_borrower,$item_7->barcode,Koha::DateUtils::output_pref({str=>$datedue6->date_due, dateformat =>'iso'}),0,$date, 0, undef );
605 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
606 is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
607 $new_log_size = Koha::ActionLogs->count( \%params_issue );
608 is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
610 $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
611 $fines->delete();
613 t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
614 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
615 is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
616 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
617 is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
620 $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
621 $hold->cancel;
623 # Bug 14101
624 # Test automatic renewal before value for "norenewalbefore" in policy is set
625 # In this case automatic renewal is not permitted prior to due date
626 my $item_4 = $builder->build_sample_item(
628 biblionumber => $biblio->biblionumber,
629 library => $branch,
630 replacementprice => 16.00,
631 itype => $itemtype,
635 $issue = AddIssue( $renewing_borrower, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
636 ( $renewokay, $error ) =
637 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
638 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
639 is( $error, 'auto_too_soon',
640 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
641 AddReserve(
643 branchcode => $branch,
644 borrowernumber => $reserving_borrowernumber,
645 biblionumber => $biblio->biblionumber,
646 itemnumber => $bibitems,
647 priority => $priority,
648 reservation_date => $resdate,
649 expiration_date => $expdate,
650 notes => $notes,
651 title => 'a title',
652 itemnumber => $item_4->itemnumber,
653 found => $found
656 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
657 is( $renewokay, 0, 'Still should not be able to renew' );
658 is( $error, 'on_reserve', 'returned code is on_reserve, reserve checked when not checking for cron' );
659 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, undef, 1 );
660 is( $renewokay, 0, 'Still should not be able to renew' );
661 is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked when checking for cron' );
662 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
663 is( $renewokay, 0, 'Still should not be able to renew' );
664 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
665 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1, 1 );
666 is( $renewokay, 0, 'Still should not be able to renew' );
667 is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
668 $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
669 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
670 is( $renewokay, 0, 'Still should not be able to renew' );
671 is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
672 ModReserveCancelAll($item_4->itemnumber, $reserving_borrowernumber);
676 $renewing_borrower_obj->autorenew_checkouts(0)->store;
677 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
678 is( $renewokay, 1, 'No renewal before is undef, but patron opted out of auto_renewal' );
679 $renewing_borrower_obj->autorenew_checkouts(1)->store;
682 # Bug 7413
683 # Test premature manual renewal
684 Koha::CirculationRules->set_rule(
686 categorycode => undef,
687 branchcode => undef,
688 itemtype => undef,
689 rule_name => 'norenewalbefore',
690 rule_value => '7',
694 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
695 is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
696 is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
698 # Bug 14395
699 # Test 'exact time' setting for syspref NoRenewalBeforePrecision
700 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
702 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
703 $datedue->clone->add( days => -7 ),
704 'Bug 14395: Renewals permitted 7 days before due date, as expected'
707 # Bug 14395
708 # Test 'date' setting for syspref NoRenewalBeforePrecision
709 t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
711 GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
712 $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
713 'Bug 14395: Renewals permitted 7 days before due date, as expected'
716 # Bug 14101
717 # Test premature automatic renewal
718 ( $renewokay, $error ) =
719 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
720 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
721 is( $error, 'auto_too_soon',
722 'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
725 $renewing_borrower_obj->autorenew_checkouts(0)->store;
726 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
727 is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
728 is( $error, 'too_soon', 'Error is too_soon, no auto' );
729 $renewing_borrower_obj->autorenew_checkouts(1)->store;
731 # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
732 # and test automatic renewal again
733 $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
734 ( $renewokay, $error ) =
735 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
736 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
737 is( $error, 'auto_too_soon',
738 'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
741 $renewing_borrower_obj->autorenew_checkouts(0)->store;
742 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
743 is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
744 is( $error, 'too_soon', 'Error is too_soon, no auto' );
745 $renewing_borrower_obj->autorenew_checkouts(1)->store;
747 # Change policy so that loans can be renewed 99 days prior to the due date
748 # and test automatic renewal again
749 $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
750 ( $renewokay, $error ) =
751 CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
752 is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
753 is( $error, 'auto_renew',
754 'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
757 $renewing_borrower_obj->autorenew_checkouts(0)->store;
758 ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
759 is( $renewokay, 1, 'No renewal before is 99, patron opted out of auto_renewal so can renew' );
760 $renewing_borrower_obj->autorenew_checkouts(1)->store;
762 subtest "too_late_renewal / no_auto_renewal_after" => sub {
763 plan tests => 14;
764 my $item_to_auto_renew = $builder->build_sample_item(
766 biblionumber => $biblio->biblionumber,
767 library => $branch,
771 my $ten_days_before = dt_from_string->add( days => -10 );
772 my $ten_days_ahead = dt_from_string->add( days => 10 );
773 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
775 Koha::CirculationRules->set_rules(
777 categorycode => undef,
778 branchcode => undef,
779 itemtype => undef,
780 rules => {
781 norenewalbefore => '7',
782 no_auto_renewal_after => '9',
786 ( $renewokay, $error ) =
787 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
788 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
789 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
791 Koha::CirculationRules->set_rules(
793 categorycode => undef,
794 branchcode => undef,
795 itemtype => undef,
796 rules => {
797 norenewalbefore => '7',
798 no_auto_renewal_after => '10',
802 ( $renewokay, $error ) =
803 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
804 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
805 is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
807 Koha::CirculationRules->set_rules(
809 categorycode => undef,
810 branchcode => undef,
811 itemtype => undef,
812 rules => {
813 norenewalbefore => '7',
814 no_auto_renewal_after => '11',
818 ( $renewokay, $error ) =
819 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
820 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
821 is( $error, 'auto_too_soon', 'Cannot auto renew, too soon - no_auto_renewal_after is defined(returned code is auto_too_soon)' );
823 Koha::CirculationRules->set_rules(
825 categorycode => undef,
826 branchcode => undef,
827 itemtype => undef,
828 rules => {
829 norenewalbefore => '10',
830 no_auto_renewal_after => '11',
834 ( $renewokay, $error ) =
835 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
836 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
837 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
839 Koha::CirculationRules->set_rules(
841 categorycode => undef,
842 branchcode => undef,
843 itemtype => undef,
844 rules => {
845 norenewalbefore => '10',
846 no_auto_renewal_after => undef,
847 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
851 ( $renewokay, $error ) =
852 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
853 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
854 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
856 Koha::CirculationRules->set_rules(
858 categorycode => undef,
859 branchcode => undef,
860 itemtype => undef,
861 rules => {
862 norenewalbefore => '7',
863 no_auto_renewal_after => '15',
864 no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
868 ( $renewokay, $error ) =
869 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
870 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
871 is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
873 Koha::CirculationRules->set_rules(
875 categorycode => undef,
876 branchcode => undef,
877 itemtype => undef,
878 rules => {
879 norenewalbefore => '10',
880 no_auto_renewal_after => undef,
881 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 1 ),
885 ( $renewokay, $error ) =
886 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
887 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
888 is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
891 subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew & OPACFineNoRenewalsIncludeCredit" => sub {
892 plan tests => 10;
893 my $item_to_auto_renew = $builder->build_sample_item(
895 biblionumber => $biblio->biblionumber,
896 library => $branch,
900 my $ten_days_before = dt_from_string->add( days => -10 );
901 my $ten_days_ahead = dt_from_string->add( days => 10 );
902 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
904 Koha::CirculationRules->set_rules(
906 categorycode => undef,
907 branchcode => undef,
908 itemtype => undef,
909 rules => {
910 norenewalbefore => '10',
911 no_auto_renewal_after => '11',
915 C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
916 C4::Context->set_preference('OPACFineNoRenewals','10');
917 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
918 my $fines_amount = 5;
919 my $account = Koha::Account->new({patron_id => $renewing_borrowernumber});
920 $account->add_debit(
922 amount => $fines_amount,
923 interface => 'test',
924 type => 'OVERDUE',
925 item_id => $item_to_auto_renew->itemnumber,
926 description => "Some fines"
928 )->status('RETURNED')->store;
929 ( $renewokay, $error ) =
930 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
931 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
932 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
934 $account->add_debit(
936 amount => $fines_amount,
937 interface => 'test',
938 type => 'OVERDUE',
939 item_id => $item_to_auto_renew->itemnumber,
940 description => "Some fines"
942 )->status('RETURNED')->store;
943 ( $renewokay, $error ) =
944 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
945 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
946 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
948 $account->add_debit(
950 amount => $fines_amount,
951 interface => 'test',
952 type => 'OVERDUE',
953 item_id => $item_to_auto_renew->itemnumber,
954 description => "Some fines"
956 )->status('RETURNED')->store;
957 ( $renewokay, $error ) =
958 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
959 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
960 is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
962 $account->add_credit(
964 amount => $fines_amount,
965 interface => 'test',
966 type => 'PAYMENT',
967 description => "Some payment"
969 )->store;
970 ( $renewokay, $error ) =
971 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
972 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
973 is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
975 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','0');
976 ( $renewokay, $error ) =
977 CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
978 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
979 is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit' );
981 $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
982 C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
985 subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
986 plan tests => 6;
987 my $item_to_auto_renew = $builder->build_sample_item(
989 biblionumber => $biblio->biblionumber,
990 library => $branch,
994 Koha::CirculationRules->set_rules(
996 categorycode => undef,
997 branchcode => undef,
998 itemtype => undef,
999 rules => {
1000 norenewalbefore => 10,
1001 no_auto_renewal_after => 11,
1006 my $ten_days_before = dt_from_string->add( days => -10 );
1007 my $ten_days_ahead = dt_from_string->add( days => 10 );
1009 # Patron is expired and BlockExpiredPatronOpacActions=0
1010 # => auto renew is allowed
1011 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
1012 my $patron = $expired_borrower;
1013 my $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1014 ( $renewokay, $error ) =
1015 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1016 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1017 is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
1018 Koha::Checkouts->find( $checkout->issue_id )->delete;
1021 # Patron is expired and BlockExpiredPatronOpacActions=1
1022 # => auto renew is not allowed
1023 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1024 $patron = $expired_borrower;
1025 $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1026 ( $renewokay, $error ) =
1027 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1028 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1029 is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
1030 Koha::Checkouts->find( $checkout->issue_id )->delete;
1033 # Patron is not expired and BlockExpiredPatronOpacActions=1
1034 # => auto renew is allowed
1035 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1036 $patron = $renewing_borrower;
1037 $checkout = AddIssue( $patron, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1038 ( $renewokay, $error ) =
1039 CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->itemnumber );
1040 is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1041 is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
1042 Koha::Checkouts->find( $checkout->issue_id )->delete;
1045 subtest "GetLatestAutoRenewDate" => sub {
1046 plan tests => 5;
1047 my $item_to_auto_renew = $builder->build_sample_item(
1049 biblionumber => $biblio->biblionumber,
1050 library => $branch,
1054 my $ten_days_before = dt_from_string->add( days => -10 );
1055 my $ten_days_ahead = dt_from_string->add( days => 10 );
1056 AddIssue( $renewing_borrower, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1057 Koha::CirculationRules->set_rules(
1059 categorycode => undef,
1060 branchcode => undef,
1061 itemtype => undef,
1062 rules => {
1063 norenewalbefore => '7',
1064 no_auto_renewal_after => '',
1065 no_auto_renewal_after_hard_limit => undef,
1069 my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1070 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' );
1071 my $five_days_before = dt_from_string->add( days => -5 );
1072 Koha::CirculationRules->set_rules(
1074 categorycode => undef,
1075 branchcode => undef,
1076 itemtype => undef,
1077 rules => {
1078 norenewalbefore => '10',
1079 no_auto_renewal_after => '5',
1080 no_auto_renewal_after_hard_limit => undef,
1084 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1085 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1086 $five_days_before->truncate( to => 'minute' ),
1087 'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
1089 my $five_days_ahead = dt_from_string->add( days => 5 );
1090 $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
1091 $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
1092 $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
1093 Koha::CirculationRules->set_rules(
1095 categorycode => undef,
1096 branchcode => undef,
1097 itemtype => undef,
1098 rules => {
1099 norenewalbefore => '10',
1100 no_auto_renewal_after => '15',
1101 no_auto_renewal_after_hard_limit => undef,
1105 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1106 is( $latest_auto_renew_date->truncate( to => 'minute' ),
1107 $five_days_ahead->truncate( to => 'minute' ),
1108 'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
1110 my $two_days_ahead = dt_from_string->add( days => 2 );
1111 Koha::CirculationRules->set_rules(
1113 categorycode => undef,
1114 branchcode => undef,
1115 itemtype => undef,
1116 rules => {
1117 norenewalbefore => '10',
1118 no_auto_renewal_after => '',
1119 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1123 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1124 is( $latest_auto_renew_date->truncate( to => 'day' ),
1125 $two_days_ahead->truncate( to => 'day' ),
1126 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
1128 Koha::CirculationRules->set_rules(
1130 categorycode => undef,
1131 branchcode => undef,
1132 itemtype => undef,
1133 rules => {
1134 norenewalbefore => '10',
1135 no_auto_renewal_after => '15',
1136 no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1140 $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->itemnumber );
1141 is( $latest_auto_renew_date->truncate( to => 'day' ),
1142 $two_days_ahead->truncate( to => 'day' ),
1143 'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
1147 # Too many renewals
1149 # set policy to forbid renewals
1150 Koha::CirculationRules->set_rules(
1152 categorycode => undef,
1153 branchcode => undef,
1154 itemtype => undef,
1155 rules => {
1156 norenewalbefore => undef,
1157 renewalsallowed => 0,
1162 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
1163 is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
1164 is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
1166 # Too many unseen renewals
1167 Koha::CirculationRules->set_rules(
1169 categorycode => undef,
1170 branchcode => undef,
1171 itemtype => undef,
1172 rules => {
1173 unseen_renewals_allowed => 2,
1174 renewalsallowed => 10,
1178 t::lib::Mocks::mock_preference('UnseenRenewals', 1);
1179 $dbh->do('UPDATE issues SET unseen_renewals = 2 where borrowernumber = ? AND itemnumber = ?', undef, ($renewing_borrowernumber, $item_1->itemnumber));
1180 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
1181 is( $renewokay, 0, 'Cannot renew, 0 unseen renewals allowed');
1182 is( $error, 'too_unseen', 'Cannot renew, returned code is too_unseen');
1183 Koha::CirculationRules->set_rules(
1185 categorycode => undef,
1186 branchcode => undef,
1187 itemtype => undef,
1188 rules => {
1189 norenewalbefore => undef,
1190 renewalsallowed => 0,
1194 t::lib::Mocks::mock_preference('UnseenRenewals', 0);
1196 # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
1197 t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
1198 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1200 C4::Overdues::UpdateFine(
1202 issue_id => $issue->id(),
1203 itemnumber => $item_1->itemnumber,
1204 borrowernumber => $renewing_borrower->{borrowernumber},
1205 amount => 15.00,
1206 type => q{},
1207 due => Koha::DateUtils::output_pref($datedue)
1211 my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower->{borrowernumber} })->next();
1212 is( $line->debit_type_code, 'OVERDUE', 'Account line type is OVERDUE' );
1213 is( $line->status, 'UNRETURNED', 'Account line status is UNRETURNED' );
1214 is( $line->amountoutstanding+0, 15, 'Account line amount outstanding is 15.00' );
1215 is( $line->amount+0, 15, 'Account line amount is 15.00' );
1216 is( $line->issue_id, $issue->id, 'Account line issue id matches' );
1218 my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
1219 is( $offset->type, 'OVERDUE', 'Account offset type is Fine' );
1220 is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
1222 t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
1223 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
1225 LostItem( $item_1->itemnumber, 'test', 1 );
1227 $line = Koha::Account::Lines->find($line->id);
1228 is( $line->debit_type_code, 'OVERDUE', 'Account type remains as OVERDUE' );
1229 isnt( $line->status, 'UNRETURNED', 'Account status correctly changed from UNRETURNED to RETURNED' );
1231 my $item = Koha::Items->find($item_1->itemnumber);
1232 ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
1233 my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
1234 is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
1236 my $total_due = $dbh->selectrow_array(
1237 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1238 undef, $renewing_borrower->{borrowernumber}
1241 is( $total_due+0, 15, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
1243 C4::Context->dbh->do("DELETE FROM accountlines");
1245 C4::Overdues::UpdateFine(
1247 issue_id => $issue2->id(),
1248 itemnumber => $item_2->itemnumber,
1249 borrowernumber => $renewing_borrower->{borrowernumber},
1250 amount => 15.00,
1251 type => q{},
1252 due => Koha::DateUtils::output_pref($datedue)
1256 LostItem( $item_2->itemnumber, 'test', 0 );
1258 my $item2 = Koha::Items->find($item_2->itemnumber);
1259 ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
1260 ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
1262 $total_due = $dbh->selectrow_array(
1263 'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1264 undef, $renewing_borrower->{borrowernumber}
1267 ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
1269 my $future = dt_from_string();
1270 $future->add( days => 7 );
1271 my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
1272 ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
1274 # Users cannot renew any item if there is an overdue item
1275 t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
1276 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
1277 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1278 ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
1279 is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1281 my $manager = $builder->build_object({ class => "Koha::Patrons" });
1282 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
1283 t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1284 $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
1285 LostItem( $item_3->itemnumber, 'test', 0 );
1286 my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
1287 is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
1289 $accountline->description,
1290 sprintf( "%s %s %s",
1291 $item_3->biblio->title || '',
1292 $item_3->barcode || '',
1293 $item_3->itemcallnumber || '' ),
1294 "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1298 subtest "GetUpcomingDueIssues" => sub {
1299 plan tests => 12;
1301 my $branch = $library2->{branchcode};
1303 #Create another record
1304 my $biblio2 = $builder->build_sample_biblio();
1306 #Create third item
1307 my $item_1 = Koha::Items->find($reused_itemnumber_1);
1308 my $item_2 = Koha::Items->find($reused_itemnumber_2);
1309 my $item_3 = $builder->build_sample_item(
1311 biblionumber => $biblio2->biblionumber,
1312 library => $branch,
1313 itype => $itemtype,
1318 # Create a borrower
1319 my %a_borrower_data = (
1320 firstname => 'Fridolyn',
1321 surname => 'SOMERS',
1322 categorycode => $patron_category->{categorycode},
1323 branchcode => $branch,
1326 my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1327 my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber )->unblessed;
1329 my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1330 my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1331 my $today = DateTime->today(time_zone => C4::Context->tz());
1333 my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1334 my $datedue = dt_from_string( $issue->date_due() );
1335 my $issue2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1336 my $datedue2 = dt_from_string( $issue->date_due() );
1338 my $upcoming_dues;
1340 # GetUpcomingDueIssues tests
1341 for my $i(0..1) {
1342 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1343 is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1346 #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1347 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1348 is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1350 for my $i(3..5) {
1351 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1352 is ( scalar( @$upcoming_dues ), 1,
1353 "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1356 # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1358 my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1360 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1361 is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1363 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1364 is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1366 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1367 is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1369 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1370 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1372 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1373 is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1375 $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1376 is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1380 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1381 my $branch = $library2->{branchcode};
1383 my $biblio = $builder->build_sample_biblio();
1385 #Create third item
1386 my $item = $builder->build_sample_item(
1388 biblionumber => $biblio->biblionumber,
1389 library => $branch,
1390 itype => $itemtype,
1394 # Create a borrower
1395 my %a_borrower_data = (
1396 firstname => 'Kyle',
1397 surname => 'Hall',
1398 categorycode => $patron_category->{categorycode},
1399 branchcode => $branch,
1402 my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1404 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1405 my $issue = AddIssue( $borrower, $item->barcode );
1406 UpdateFine(
1408 issue_id => $issue->id(),
1409 itemnumber => $item->itemnumber,
1410 borrowernumber => $borrowernumber,
1411 amount => 0,
1412 type => q{}
1416 my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1417 my $count = $hr->{count};
1419 is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1422 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1423 $dbh->do('DELETE FROM issues');
1424 $dbh->do('DELETE FROM items');
1425 $dbh->do('DELETE FROM circulation_rules');
1426 Koha::CirculationRules->set_rules(
1428 categorycode => undef,
1429 itemtype => undef,
1430 branchcode => undef,
1431 rules => {
1432 reservesallowed => 25,
1433 issuelength => 14,
1434 lengthunit => 'days',
1435 renewalsallowed => 1,
1436 renewalperiod => 7,
1437 norenewalbefore => undef,
1438 auto_renew => 0,
1439 fine => .10,
1440 chargeperiod => 1,
1441 maxissueqty => 20
1445 my $biblio = $builder->build_sample_biblio();
1447 my $item_1 = $builder->build_sample_item(
1449 biblionumber => $biblio->biblionumber,
1450 library => $library2->{branchcode},
1451 itype => $itemtype,
1455 my $item_2= $builder->build_sample_item(
1457 biblionumber => $biblio->biblionumber,
1458 library => $library2->{branchcode},
1459 itype => $itemtype,
1463 my $borrowernumber1 = Koha::Patron->new({
1464 firstname => 'Kyle',
1465 surname => 'Hall',
1466 categorycode => $patron_category->{categorycode},
1467 branchcode => $library2->{branchcode},
1468 })->store->borrowernumber;
1469 my $borrowernumber2 = Koha::Patron->new({
1470 firstname => 'Chelsea',
1471 surname => 'Hall',
1472 categorycode => $patron_category->{categorycode},
1473 branchcode => $library2->{branchcode},
1474 })->store->borrowernumber;
1476 my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
1477 my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
1479 my $issue = AddIssue( $borrower1, $item_1->barcode );
1481 my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1482 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1484 AddReserve(
1486 branchcode => $library2->{branchcode},
1487 borrowernumber => $borrowernumber2,
1488 biblionumber => $biblio->biblionumber,
1489 priority => 1,
1493 Koha::CirculationRules->set_rules(
1495 categorycode => undef,
1496 itemtype => undef,
1497 branchcode => undef,
1498 rules => {
1499 onshelfholds => 0,
1503 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1504 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1505 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1507 Koha::CirculationRules->set_rules(
1509 categorycode => undef,
1510 itemtype => undef,
1511 branchcode => undef,
1512 rules => {
1513 onshelfholds => 0,
1517 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1518 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1519 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1521 Koha::CirculationRules->set_rules(
1523 categorycode => undef,
1524 itemtype => undef,
1525 branchcode => undef,
1526 rules => {
1527 onshelfholds => 1,
1531 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1532 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1533 is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1535 Koha::CirculationRules->set_rules(
1537 categorycode => undef,
1538 itemtype => undef,
1539 branchcode => undef,
1540 rules => {
1541 onshelfholds => 1,
1545 t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1546 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1547 is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1549 # Setting item not checked out to be not for loan but holdable
1550 $item_2->notforloan(-1)->store;
1552 ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1553 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' );
1557 # Don't allow renewing onsite checkout
1558 my $branch = $library->{branchcode};
1560 #Create another record
1561 my $biblio = $builder->build_sample_biblio();
1563 my $item = $builder->build_sample_item(
1565 biblionumber => $biblio->biblionumber,
1566 library => $branch,
1567 itype => $itemtype,
1571 my $borrowernumber = Koha::Patron->new({
1572 firstname => 'fn',
1573 surname => 'dn',
1574 categorycode => $patron_category->{categorycode},
1575 branchcode => $branch,
1576 })->store->borrowernumber;
1578 my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1580 my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
1581 my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $item->itemnumber );
1582 is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
1583 is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
1587 my $library = $builder->build({ source => 'Branch' });
1589 my $biblio = $builder->build_sample_biblio();
1591 my $item = $builder->build_sample_item(
1593 biblionumber => $biblio->biblionumber,
1594 library => $library->{branchcode},
1595 itype => $itemtype,
1599 my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
1601 my $issue = AddIssue( $patron, $item->barcode );
1602 UpdateFine(
1604 issue_id => $issue->id(),
1605 itemnumber => $item->itemnumber,
1606 borrowernumber => $patron->{borrowernumber},
1607 amount => 1,
1608 type => q{}
1611 UpdateFine(
1613 issue_id => $issue->id(),
1614 itemnumber => $item->itemnumber,
1615 borrowernumber => $patron->{borrowernumber},
1616 amount => 2,
1617 type => q{}
1620 is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1623 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1624 plan tests => 24;
1626 my $homebranch = $builder->build( { source => 'Branch' } );
1627 my $holdingbranch = $builder->build( { source => 'Branch' } );
1628 my $otherbranch = $builder->build( { source => 'Branch' } );
1629 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1630 my $patron_2 = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1632 my $item = $builder->build_sample_item(
1634 homebranch => $homebranch->{branchcode},
1635 holdingbranch => $holdingbranch->{branchcode},
1639 set_userenv($holdingbranch);
1641 my $issue = AddIssue( $patron_1->unblessed, $item->barcode );
1642 is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
1644 my ( $error, $question, $alerts );
1646 # AllowReturnToBranch == anywhere
1647 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1648 ## Test that unknown barcodes don't generate internal server errors
1649 set_userenv($homebranch);
1650 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
1651 ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
1652 ## Can be issued from homebranch
1653 set_userenv($homebranch);
1654 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1655 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1656 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1657 ## Can be issued from holdingbranch
1658 set_userenv($holdingbranch);
1659 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1660 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1661 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1662 ## Can be issued from another branch
1663 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1664 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1665 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1667 # AllowReturnToBranch == holdingbranch
1668 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1669 ## Cannot be issued from homebranch
1670 set_userenv($homebranch);
1671 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1672 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1673 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1674 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
1675 ## Can be issued from holdinbranch
1676 set_userenv($holdingbranch);
1677 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1678 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1679 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1680 ## Cannot be issued from another branch
1681 set_userenv($otherbranch);
1682 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1683 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1684 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1685 is( $error->{branch_to_return}, $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
1687 # AllowReturnToBranch == homebranch
1688 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1689 ## Can be issued from holdinbranch
1690 set_userenv($homebranch);
1691 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1692 is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1693 is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1694 ## Cannot be issued from holdinbranch
1695 set_userenv($holdingbranch);
1696 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1697 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1698 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1699 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1700 ## Cannot be issued from holdinbranch
1701 set_userenv($otherbranch);
1702 ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
1703 is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1704 is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1705 is( $error->{branch_to_return}, $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1707 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1710 subtest 'AddIssue & AllowReturnToBranch' => sub {
1711 plan tests => 9;
1713 my $homebranch = $builder->build( { source => 'Branch' } );
1714 my $holdingbranch = $builder->build( { source => 'Branch' } );
1715 my $otherbranch = $builder->build( { source => 'Branch' } );
1716 my $patron_1 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1717 my $patron_2 = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1719 my $item = $builder->build_sample_item(
1721 homebranch => $homebranch->{branchcode},
1722 holdingbranch => $holdingbranch->{branchcode},
1726 set_userenv($holdingbranch);
1728 my $ref_issue = 'Koha::Checkout';
1729 my $issue = AddIssue( $patron_1, $item->barcode );
1731 my ( $error, $question, $alerts );
1733 # AllowReturnToBranch == homebranch
1734 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1735 ## Can be issued from homebranch
1736 set_userenv($homebranch);
1737 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
1738 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1739 ## Can be issued from holdinbranch
1740 set_userenv($holdingbranch);
1741 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
1742 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1743 ## Can be issued from another branch
1744 set_userenv($otherbranch);
1745 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
1746 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1748 # AllowReturnToBranch == holdinbranch
1749 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1750 ## Cannot be issued from homebranch
1751 set_userenv($homebranch);
1752 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
1753 ## Can be issued from holdingbranch
1754 set_userenv($holdingbranch);
1755 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
1756 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1757 ## Cannot be issued from another branch
1758 set_userenv($otherbranch);
1759 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
1761 # AllowReturnToBranch == homebranch
1762 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1763 ## Can be issued from homebranch
1764 set_userenv($homebranch);
1765 is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
1766 set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
1767 ## Cannot be issued from holdinbranch
1768 set_userenv($holdingbranch);
1769 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
1770 ## Cannot be issued from another branch
1771 set_userenv($otherbranch);
1772 is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
1773 # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1776 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1777 plan tests => 8;
1779 my $library = $builder->build( { source => 'Branch' } );
1780 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1781 my $item_1 = $builder->build_sample_item(
1783 library => $library->{branchcode},
1786 my $item_2 = $builder->build_sample_item(
1788 library => $library->{branchcode},
1792 my ( $error, $question, $alerts );
1794 # Patron cannot issue item_1, they have overdues
1795 my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1796 my $issue = AddIssue( $patron->unblessed, $item_1->barcode, $yesterday ); # Add an overdue
1798 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1799 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1800 is( keys(%$error) + keys(%$alerts), 0, 'No key for error and alert' . str($error, $question, $alerts) );
1801 is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
1803 t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1804 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1805 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1806 is( $error->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
1808 # Patron cannot issue item_1, they are debarred
1809 my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1810 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
1811 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1812 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1813 is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
1815 Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
1816 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1817 is( keys(%$question) + keys(%$alerts), 0, 'No key for question and alert ' . str($error, $question, $alerts) );
1818 is( $error->{USERBLOCKEDNOENDDATE}, '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
1821 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
1822 plan tests => 1;
1824 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1825 my $patron_category_x = $builder->build_object(
1827 class => 'Koha::Patron::Categories',
1828 value => { category_type => 'X' }
1831 my $patron = $builder->build_object(
1833 class => 'Koha::Patrons',
1834 value => {
1835 categorycode => $patron_category_x->categorycode,
1836 gonenoaddress => undef,
1837 lost => undef,
1838 debarred => undef,
1839 borrowernotes => ""
1843 my $item_1 = $builder->build_sample_item(
1845 library => $library->{branchcode},
1849 my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->barcode );
1850 is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
1852 # TODO There are other tests to provide here
1855 subtest 'MultipleReserves' => sub {
1856 plan tests => 3;
1858 my $biblio = $builder->build_sample_biblio();
1860 my $branch = $library2->{branchcode};
1862 my $item_1 = $builder->build_sample_item(
1864 biblionumber => $biblio->biblionumber,
1865 library => $branch,
1866 replacementprice => 12.00,
1867 itype => $itemtype,
1871 my $item_2 = $builder->build_sample_item(
1873 biblionumber => $biblio->biblionumber,
1874 library => $branch,
1875 replacementprice => 12.00,
1876 itype => $itemtype,
1880 my $bibitems = '';
1881 my $priority = '1';
1882 my $resdate = undef;
1883 my $expdate = undef;
1884 my $notes = '';
1885 my $checkitem = undef;
1886 my $found = undef;
1888 my %renewing_borrower_data = (
1889 firstname => 'John',
1890 surname => 'Renewal',
1891 categorycode => $patron_category->{categorycode},
1892 branchcode => $branch,
1894 my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
1895 my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
1896 my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
1897 my $datedue = dt_from_string( $issue->date_due() );
1898 is (defined $issue->date_due(), 1, "item 1 checked out");
1899 my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
1901 my %reserving_borrower_data1 = (
1902 firstname => 'Katrin',
1903 surname => 'Reservation',
1904 categorycode => $patron_category->{categorycode},
1905 branchcode => $branch,
1907 my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
1908 AddReserve(
1910 branchcode => $branch,
1911 borrowernumber => $reserving_borrowernumber1,
1912 biblionumber => $biblio->biblionumber,
1913 priority => $priority,
1914 reservation_date => $resdate,
1915 expiration_date => $expdate,
1916 notes => $notes,
1917 itemnumber => $checkitem,
1918 found => $found,
1922 my %reserving_borrower_data2 = (
1923 firstname => 'Kirk',
1924 surname => 'Reservation',
1925 categorycode => $patron_category->{categorycode},
1926 branchcode => $branch,
1928 my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
1929 AddReserve(
1931 branchcode => $branch,
1932 borrowernumber => $reserving_borrowernumber2,
1933 biblionumber => $biblio->biblionumber,
1934 priority => $priority,
1935 reservation_date => $resdate,
1936 expiration_date => $expdate,
1937 notes => $notes,
1938 itemnumber => $checkitem,
1939 found => $found,
1944 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1945 is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
1948 my $item_3 = $builder->build_sample_item(
1950 biblionumber => $biblio->biblionumber,
1951 library => $branch,
1952 replacementprice => 12.00,
1953 itype => $itemtype,
1958 my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1959 is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
1963 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
1964 plan tests => 5;
1966 my $library = $builder->build( { source => 'Branch' } );
1967 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1969 my $biblionumber = $builder->build_sample_biblio(
1971 branchcode => $library->{branchcode},
1973 )->biblionumber;
1974 my $item_1 = $builder->build_sample_item(
1976 biblionumber => $biblionumber,
1977 library => $library->{branchcode},
1981 my $item_2 = $builder->build_sample_item(
1983 biblionumber => $biblionumber,
1984 library => $library->{branchcode},
1988 my ( $error, $question, $alerts );
1989 my $issue = AddIssue( $patron->unblessed, $item_1->barcode, dt_from_string->add( days => 1 ) );
1991 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1992 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
1993 cmp_deeply(
1994 { error => $error, alerts => $alerts },
1995 { error => {}, alerts => {} },
1996 'No error or alert should be raised'
1998 is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
2000 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2001 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2002 cmp_deeply(
2003 { error => $error, question => $question, alerts => $alerts },
2004 { error => {}, question => {}, alerts => {} },
2005 'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
2008 # Add a subscription
2009 Koha::Subscription->new({ biblionumber => $biblionumber })->store;
2011 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2012 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2013 cmp_deeply(
2014 { error => $error, question => $question, alerts => $alerts },
2015 { error => {}, question => {}, alerts => {} },
2016 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2019 t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2020 ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2021 cmp_deeply(
2022 { error => $error, question => $question, alerts => $alerts },
2023 { error => {}, question => {}, alerts => {} },
2024 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2028 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
2029 plan tests => 8;
2031 my $library = $builder->build( { source => 'Branch' } );
2032 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2034 # Add 2 items
2035 my $biblionumber = $builder->build_sample_biblio(
2037 branchcode => $library->{branchcode},
2039 )->biblionumber;
2040 my $item_1 = $builder->build_sample_item(
2042 biblionumber => $biblionumber,
2043 library => $library->{branchcode},
2046 my $item_2 = $builder->build_sample_item(
2048 biblionumber => $biblionumber,
2049 library => $library->{branchcode},
2053 # And the circulation rule
2054 Koha::CirculationRules->search->delete;
2055 Koha::CirculationRules->set_rules(
2057 categorycode => undef,
2058 itemtype => undef,
2059 branchcode => undef,
2060 rules => {
2061 issuelength => 1,
2062 firstremind => 1, # 1 day of grace
2063 finedays => 2, # 2 days of fine per day of overdue
2064 lengthunit => 'days',
2069 # Patron cannot issue item_1, they have overdues
2070 my $now = dt_from_string;
2071 my $five_days_ago = $now->clone->subtract( days => 5 );
2072 my $ten_days_ago = $now->clone->subtract( days => 10 );
2073 AddIssue( $patron, $item_1->barcode, $five_days_ago ); # Add an overdue
2074 AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2075 ; # Add another overdue
2077 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2078 AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2079 my $debarments = Koha::Patron::Debarments::GetDebarments(
2080 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2081 is( scalar(@$debarments), 1 );
2083 # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2084 # Same for the others
2085 my $expected_expiration = output_pref(
2087 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2088 dateformat => 'sql',
2089 dateonly => 1
2092 is( $debarments->[0]->{expiration}, $expected_expiration );
2094 AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2095 $debarments = Koha::Patron::Debarments::GetDebarments(
2096 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2097 is( scalar(@$debarments), 1 );
2098 $expected_expiration = output_pref(
2100 dt => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2101 dateformat => 'sql',
2102 dateonly => 1
2105 is( $debarments->[0]->{expiration}, $expected_expiration );
2107 Koha::Patron::Debarments::DelUniqueDebarment(
2108 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2110 t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2111 AddIssue( $patron, $item_1->barcode, $five_days_ago ); # Add an overdue
2112 AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2113 ; # Add another overdue
2114 AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2115 $debarments = Koha::Patron::Debarments::GetDebarments(
2116 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2117 is( scalar(@$debarments), 1 );
2118 $expected_expiration = output_pref(
2120 dt => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2121 dateformat => 'sql',
2122 dateonly => 1
2125 is( $debarments->[0]->{expiration}, $expected_expiration );
2127 AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2128 $debarments = Koha::Patron::Debarments::GetDebarments(
2129 { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2130 is( scalar(@$debarments), 1 );
2131 $expected_expiration = output_pref(
2133 dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
2134 dateformat => 'sql',
2135 dateonly => 1
2138 is( $debarments->[0]->{expiration}, $expected_expiration );
2141 subtest 'AddReturn + suspension_chargeperiod' => sub {
2142 plan tests => 27;
2144 my $library = $builder->build( { source => 'Branch' } );
2145 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2147 my $biblionumber = $builder->build_sample_biblio(
2149 branchcode => $library->{branchcode},
2151 )->biblionumber;
2152 my $item_1 = $builder->build_sample_item(
2154 biblionumber => $biblionumber,
2155 library => $library->{branchcode},
2159 # And the issuing rule
2160 Koha::CirculationRules->search->delete;
2161 Koha::CirculationRules->set_rules(
2163 categorycode => '*',
2164 itemtype => '*',
2165 branchcode => '*',
2166 rules => {
2167 issuelength => 1,
2168 firstremind => 0, # 0 day of grace
2169 finedays => 2, # 2 days of fine per day of overdue
2170 suspension_chargeperiod => 1,
2171 lengthunit => 'days',
2176 my $now = dt_from_string;
2177 my $five_days_ago = $now->clone->subtract( days => 5 );
2178 # We want to charge 2 days every day, without grace
2179 # With 5 days of overdue: 5 * Z
2180 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2181 test_debarment_on_checkout(
2183 item => $item_1,
2184 library => $library,
2185 patron => $patron,
2186 due_date => $five_days_ago,
2187 expiration_date => $expected_expiration,
2191 # Same with undef firstremind
2192 Koha::CirculationRules->search->delete;
2193 Koha::CirculationRules->set_rules(
2195 categorycode => '*',
2196 itemtype => '*',
2197 branchcode => '*',
2198 rules => {
2199 issuelength => 1,
2200 firstremind => undef, # 0 day of grace
2201 finedays => 2, # 2 days of fine per day of overdue
2202 suspension_chargeperiod => 1,
2203 lengthunit => 'days',
2208 my $now = dt_from_string;
2209 my $five_days_ago = $now->clone->subtract( days => 5 );
2210 # We want to charge 2 days every day, without grace
2211 # With 5 days of overdue: 5 * Z
2212 my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2213 test_debarment_on_checkout(
2215 item => $item_1,
2216 library => $library,
2217 patron => $patron,
2218 due_date => $five_days_ago,
2219 expiration_date => $expected_expiration,
2223 # We want to charge 2 days every 2 days, without grace
2224 # With 5 days of overdue: (5 * 2) / 2
2225 Koha::CirculationRules->set_rule(
2227 categorycode => undef,
2228 branchcode => undef,
2229 itemtype => undef,
2230 rule_name => 'suspension_chargeperiod',
2231 rule_value => '2',
2235 $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
2236 test_debarment_on_checkout(
2238 item => $item_1,
2239 library => $library,
2240 patron => $patron,
2241 due_date => $five_days_ago,
2242 expiration_date => $expected_expiration,
2246 # We want to charge 2 days every 3 days, with 1 day of grace
2247 # With 5 days of overdue: ((5-1) / 3 ) * 2
2248 Koha::CirculationRules->set_rules(
2250 categorycode => undef,
2251 branchcode => undef,
2252 itemtype => undef,
2253 rules => {
2254 suspension_chargeperiod => 3,
2255 firstremind => 1,
2259 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
2260 test_debarment_on_checkout(
2262 item => $item_1,
2263 library => $library,
2264 patron => $patron,
2265 due_date => $five_days_ago,
2266 expiration_date => $expected_expiration,
2270 # Use finesCalendar to know if holiday must be skipped to calculate the due date
2271 # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
2272 Koha::CirculationRules->set_rules(
2274 categorycode => undef,
2275 branchcode => undef,
2276 itemtype => undef,
2277 rules => {
2278 finedays => 2,
2279 suspension_chargeperiod => 1,
2280 firstremind => 0,
2284 t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
2285 t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
2287 # Adding a holiday 2 days ago
2288 my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
2289 my $two_days_ago = $now->clone->subtract( days => 2 );
2290 $calendar->insert_single_holiday(
2291 day => $two_days_ago->day,
2292 month => $two_days_ago->month,
2293 year => $two_days_ago->year,
2294 title => 'holidayTest-2d',
2295 description => 'holidayDesc 2 days ago'
2297 # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
2298 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
2299 test_debarment_on_checkout(
2301 item => $item_1,
2302 library => $library,
2303 patron => $patron,
2304 due_date => $five_days_ago,
2305 expiration_date => $expected_expiration,
2309 # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
2310 my $two_days_ahead = $now->clone->add( days => 2 );
2311 $calendar->insert_single_holiday(
2312 day => $two_days_ahead->day,
2313 month => $two_days_ahead->month,
2314 year => $two_days_ahead->year,
2315 title => 'holidayTest+2d',
2316 description => 'holidayDesc 2 days ahead'
2319 # Same as above, but we should skip D+2
2320 $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
2321 test_debarment_on_checkout(
2323 item => $item_1,
2324 library => $library,
2325 patron => $patron,
2326 due_date => $five_days_ago,
2327 expiration_date => $expected_expiration,
2331 # Adding another holiday, day of expiration date
2332 my $expected_expiration_dt = dt_from_string($expected_expiration);
2333 $calendar->insert_single_holiday(
2334 day => $expected_expiration_dt->day,
2335 month => $expected_expiration_dt->month,
2336 year => $expected_expiration_dt->year,
2337 title => 'holidayTest_exp',
2338 description => 'holidayDesc on expiration date'
2340 # Expiration date will be the day after
2341 test_debarment_on_checkout(
2343 item => $item_1,
2344 library => $library,
2345 patron => $patron,
2346 due_date => $five_days_ago,
2347 expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
2351 test_debarment_on_checkout(
2353 item => $item_1,
2354 library => $library,
2355 patron => $patron,
2356 return_date => $now->clone->add(days => 5),
2357 expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
2361 test_debarment_on_checkout(
2363 item => $item_1,
2364 library => $library,
2365 patron => $patron,
2366 due_date => $now->clone->add(days => 1),
2367 return_date => $now->clone->add(days => 5),
2368 expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
2374 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
2375 plan tests => 2;
2377 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2378 my $patron1 = $builder->build_object(
2380 class => 'Koha::Patrons',
2381 value => {
2382 library => $library->branchcode,
2383 categorycode => $patron_category->{categorycode}
2387 my $patron2 = $builder->build_object(
2389 class => 'Koha::Patrons',
2390 value => {
2391 library => $library->branchcode,
2392 categorycode => $patron_category->{categorycode}
2397 t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
2399 my $item = $builder->build_sample_item(
2401 library => $library->branchcode,
2405 my ( $error, $question, $alerts );
2406 my $issue = AddIssue( $patron1->unblessed, $item->barcode );
2408 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2409 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
2410 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' );
2412 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
2413 ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
2414 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' );
2416 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2420 subtest 'AddReturn | is_overdue' => sub {
2421 plan tests => 9;
2423 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
2424 t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
2425 t::lib::Mocks::mock_preference('finesMode', 'production');
2426 t::lib::Mocks::mock_preference('MaxFine', '100');
2428 my $library = $builder->build( { source => 'Branch' } );
2429 my $patron = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2430 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2431 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
2433 my $item = $builder->build_sample_item(
2435 library => $library->{branchcode},
2436 replacementprice => 7
2440 Koha::CirculationRules->search->delete;
2441 Koha::CirculationRules->set_rules(
2443 categorycode => undef,
2444 itemtype => undef,
2445 branchcode => undef,
2446 rules => {
2447 issuelength => 6,
2448 lengthunit => 'days',
2449 fine => 1, # Charge 1 every day of overdue
2450 chargeperiod => 1,
2455 my $now = dt_from_string;
2456 my $one_day_ago = $now->clone->subtract( days => 1 );
2457 my $two_days_ago = $now->clone->subtract( days => 2 );
2458 my $five_days_ago = $now->clone->subtract( days => 5 );
2459 my $ten_days_ago = $now->clone->subtract( days => 10 );
2460 $patron = Koha::Patrons->find( $patron->{borrowernumber} );
2462 # No return date specified, today will be used => 10 days overdue charged
2463 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2464 AddReturn( $item->barcode, $library->{branchcode} );
2465 is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
2466 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2468 # specify return date 5 days before => no overdue charged
2469 AddIssue( $patron->unblessed, $item->barcode, $five_days_ago ); # date due was 5d ago
2470 AddReturn( $item->barcode, $library->{branchcode}, undef, $ten_days_ago );
2471 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2472 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2474 # specify return date 5 days later => 5 days overdue charged
2475 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2476 AddReturn( $item->barcode, $library->{branchcode}, undef, $five_days_ago );
2477 is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
2478 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2480 # specify return date 5 days later, specify exemptfine => no overdue charge
2481 AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2482 AddReturn( $item->barcode, $library->{branchcode}, 1, $five_days_ago );
2483 is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2484 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2486 subtest 'bug 22877 | Lost item return' => sub {
2488 plan tests => 3;
2490 my $issue = AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago ); # date due was 10d ago
2492 # Fake fines cronjob on this checkout
2493 my ($fine) =
2494 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2495 $ten_days_ago, $now );
2496 UpdateFine(
2498 issue_id => $issue->issue_id,
2499 itemnumber => $item->itemnumber,
2500 borrowernumber => $patron->borrowernumber,
2501 amount => $fine,
2502 due => output_pref($ten_days_ago)
2505 is( int( $patron->account->balance() ),
2506 10, "Overdue fine of 10 days overdue" );
2508 # Fake longoverdue with charge and not marking returned
2509 LostItem( $item->itemnumber, 'cronjob', 0 );
2510 is( int( $patron->account->balance() ),
2511 17, "Lost fine of 7 plus 10 days overdue" );
2513 # Now we return it today
2514 AddReturn( $item->barcode, $library->{branchcode} );
2515 is( int( $patron->account->balance() ),
2516 17, "Should have a single 10 days overdue fine and lost charge" );
2518 # Cleanup
2519 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2522 subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
2524 plan tests => 17;
2526 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2528 my $issue = AddIssue( $patron->unblessed, $item->barcode, $one_day_ago ); # date due was 1d ago
2530 # Fake fines cronjob on this checkout
2531 my ($fine) =
2532 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2533 $one_day_ago, $now );
2534 UpdateFine(
2536 issue_id => $issue->issue_id,
2537 itemnumber => $item->itemnumber,
2538 borrowernumber => $patron->borrowernumber,
2539 amount => $fine,
2540 due => output_pref($one_day_ago)
2543 is( int( $patron->account->balance() ),
2544 1, "Overdue fine of 1 day overdue" );
2546 # Backdated return (dropbox mode example - charge should be removed)
2547 AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
2548 is( int( $patron->account->balance() ),
2549 0, "Overdue fine should be annulled" );
2550 my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2551 is( $lines->count, 0, "Overdue fine accountline has been removed");
2553 $issue = AddIssue( $patron->unblessed, $item->barcode, $two_days_ago ); # date due was 2d ago
2555 # Fake fines cronjob on this checkout
2556 ($fine) =
2557 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2558 $two_days_ago, $now );
2559 UpdateFine(
2561 issue_id => $issue->issue_id,
2562 itemnumber => $item->itemnumber,
2563 borrowernumber => $patron->borrowernumber,
2564 amount => $fine,
2565 due => output_pref($one_day_ago)
2568 is( int( $patron->account->balance() ),
2569 2, "Overdue fine of 2 days overdue" );
2571 # Payment made against fine
2572 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2573 my $debit = $lines->next;
2574 my $credit = $patron->account->add_credit(
2576 amount => 2,
2577 type => 'PAYMENT',
2578 interface => 'test',
2581 $credit->apply(
2582 { debits => [ $debit ], offset_type => 'Payment' } );
2584 is( int( $patron->account->balance() ),
2585 0, "Overdue fine should be paid off" );
2586 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2587 is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
2588 my $line = $lines->next;
2589 is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
2590 is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
2592 # Backdated return (dropbox mode example - charge should be removed)
2593 AddReturn( $item->barcode, $library->{branchcode}, undef, $one_day_ago );
2594 is( int( $patron->account->balance() ),
2595 -1, "Refund credit has been applied" );
2596 $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
2597 is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
2599 $line = $lines->next;
2600 is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
2601 is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
2602 is($line->status,'RETURNED', "Overdue fine is fixed");
2603 $line = $lines->next;
2604 is($line->amount+0,-2, "Original payment amount remains as 2");
2605 is($line->amountoutstanding+0,0, "Original payment remains applied");
2606 $line = $lines->next;
2607 is($line->amount+0,-1, "Refund amount correctly set to 1");
2608 is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
2610 # Cleanup
2611 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2614 subtest 'bug 25417 | backdated return + exemptfine' => sub {
2616 plan tests => 2;
2618 t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2620 my $issue = AddIssue( $patron->unblessed, $item->barcode, $one_day_ago ); # date due was 1d ago
2622 # Fake fines cronjob on this checkout
2623 my ($fine) =
2624 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2625 $one_day_ago, $now );
2626 UpdateFine(
2628 issue_id => $issue->issue_id,
2629 itemnumber => $item->itemnumber,
2630 borrowernumber => $patron->borrowernumber,
2631 amount => $fine,
2632 due => output_pref($one_day_ago)
2635 is( int( $patron->account->balance() ),
2636 1, "Overdue fine of 1 day overdue" );
2638 # Backdated return (dropbox mode example - charge should no longer exist)
2639 AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
2640 is( int( $patron->account->balance() ),
2641 0, "Overdue fine should be annulled" );
2643 # Cleanup
2644 Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2647 subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
2648 plan tests => 7;
2650 t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
2652 my $due_date = dt_from_string;
2653 my $issue = AddIssue( $patron->unblessed, $item->barcode, $due_date );
2655 # Add fine
2656 UpdateFine(
2658 issue_id => $issue->issue_id,
2659 itemnumber => $item->itemnumber,
2660 borrowernumber => $patron->borrowernumber,
2661 amount => 0.25,
2662 due => output_pref($due_date)
2665 is( $patron->account->balance(),
2666 0.25, 'Overdue fine of $0.25 recorded' );
2668 # Backdate return to exact due date and time
2669 my ( undef, $message ) =
2670 AddReturn( $item->barcode, $library->{branchcode},
2671 undef, $due_date );
2673 my $accountline =
2674 Koha::Account::Lines->find( { issue_id => $issue->id } );
2675 ok( !$accountline, 'accountline removed as expected' );
2677 # Re-issue
2678 $issue = AddIssue( $patron->unblessed, $item->barcode, $due_date );
2680 # Add fine
2681 UpdateFine(
2683 issue_id => $issue->issue_id,
2684 itemnumber => $item->itemnumber,
2685 borrowernumber => $patron->borrowernumber,
2686 amount => .25,
2687 due => output_pref($due_date)
2690 is( $patron->account->balance(),
2691 0.25, 'Overdue fine of $0.25 recorded' );
2693 # Partial pay accruing fine
2694 my $lines = Koha::Account::Lines->search(
2696 borrowernumber => $patron->borrowernumber,
2697 issue_id => $issue->id
2700 my $debit = $lines->next;
2701 my $credit = $patron->account->add_credit(
2703 amount => .20,
2704 type => 'PAYMENT',
2705 interface => 'test',
2708 $credit->apply( { debits => [$debit], offset_type => 'Payment' } );
2710 is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
2712 # Backdate return to exact due date and time
2713 ( undef, $message ) =
2714 AddReturn( $item->barcode, $library->{branchcode},
2715 undef, $due_date );
2717 $lines = Koha::Account::Lines->search(
2719 borrowernumber => $patron->borrowernumber,
2720 issue_id => $issue->id
2723 $accountline = $lines->next;
2724 is( $accountline->amountoutstanding + 0,
2725 0, 'Partially paid fee amount outstanding was reduced to 0' );
2726 is( $accountline->amount + 0,
2727 0, 'Partially paid fee amount was reduced to 0' );
2728 is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
2730 # Cleanup
2731 Koha::Account::Lines->search(
2732 { borrowernumber => $patron->borrowernumber } )->delete;
2735 subtest 'enh 23091 | Lost item return policies' => sub {
2736 plan tests => 4;
2738 my $manager = $builder->build_object({ class => "Koha::Patrons" });
2740 my $branchcode_false =
2741 $builder->build( { source => 'Branch' } )->{branchcode};
2742 my $specific_rule_false = $builder->build(
2744 source => 'CirculationRule',
2745 value => {
2746 branchcode => $branchcode_false,
2747 categorycode => undef,
2748 itemtype => undef,
2749 rule_name => 'lostreturn',
2750 rule_value => 0
2754 my $branchcode_refund =
2755 $builder->build( { source => 'Branch' } )->{branchcode};
2756 my $specific_rule_refund = $builder->build(
2758 source => 'CirculationRule',
2759 value => {
2760 branchcode => $branchcode_refund,
2761 categorycode => undef,
2762 itemtype => undef,
2763 rule_name => 'lostreturn',
2764 rule_value => 'refund'
2768 my $branchcode_restore =
2769 $builder->build( { source => 'Branch' } )->{branchcode};
2770 my $specific_rule_restore = $builder->build(
2772 source => 'CirculationRule',
2773 value => {
2774 branchcode => $branchcode_restore,
2775 categorycode => undef,
2776 itemtype => undef,
2777 rule_name => 'lostreturn',
2778 rule_value => 'restore'
2782 my $branchcode_charge =
2783 $builder->build( { source => 'Branch' } )->{branchcode};
2784 my $specific_rule_charge = $builder->build(
2786 source => 'CirculationRule',
2787 value => {
2788 branchcode => $branchcode_charge,
2789 categorycode => undef,
2790 itemtype => undef,
2791 rule_name => 'lostreturn',
2792 rule_value => 'charge'
2797 my $replacement_amount = 99.00;
2798 t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2799 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
2800 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
2801 t::lib::Mocks::mock_preference( 'BlockReturnOfLostItems', 0 );
2802 t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl',
2803 'CheckinLibrary' );
2804 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge',
2805 undef );
2807 subtest 'lostreturn | false' => sub {
2808 plan tests => 12;
2810 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_false });
2812 my $item = $builder->build_sample_item(
2814 replacementprice => $replacement_amount
2818 # Issue the item
2819 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
2821 # Fake fines cronjob on this checkout
2822 my ($fine) =
2823 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2824 $ten_days_ago, $now );
2825 UpdateFine(
2827 issue_id => $issue->issue_id,
2828 itemnumber => $item->itemnumber,
2829 borrowernumber => $patron->borrowernumber,
2830 amount => $fine,
2831 due => output_pref($ten_days_ago)
2834 my $overdue_fees = Koha::Account::Lines->search(
2836 borrowernumber => $patron->id,
2837 itemnumber => $item->itemnumber,
2838 debit_type_code => 'OVERDUE'
2841 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
2842 my $overdue_fee = $overdue_fees->next;
2843 is( $overdue_fee->amount + 0,
2844 10, 'The right OVERDUE amount is generated' );
2845 is( $overdue_fee->amountoutstanding + 0,
2847 'The right OVERDUE amountoutstanding is generated' );
2849 # Simulate item marked as lost
2850 $item->itemlost(3)->store;
2851 C4::Circulation::LostItem( $item->itemnumber, 1 );
2853 my $lost_fee_lines = Koha::Account::Lines->search(
2855 borrowernumber => $patron->id,
2856 itemnumber => $item->itemnumber,
2857 debit_type_code => 'LOST'
2860 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
2861 my $lost_fee_line = $lost_fee_lines->next;
2862 is( $lost_fee_line->amount + 0,
2863 $replacement_amount, 'The right LOST amount is generated' );
2864 is( $lost_fee_line->amountoutstanding + 0,
2865 $replacement_amount,
2866 'The right LOST amountoutstanding is generated' );
2867 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2869 # Return lost item
2870 my ( $returned, $message ) =
2871 AddReturn( $item->barcode, $branchcode_false, undef, $five_days_ago );
2873 $overdue_fee->discard_changes;
2874 is( $overdue_fee->amount + 0,
2875 10, 'The OVERDUE amount is left intact' );
2876 is( $overdue_fee->amountoutstanding + 0,
2878 'The OVERDUE amountoutstanding is left intact' );
2880 $lost_fee_line->discard_changes;
2881 is( $lost_fee_line->amount + 0,
2882 $replacement_amount, 'The LOST amount is left intact' );
2883 is( $lost_fee_line->amountoutstanding + 0,
2884 $replacement_amount,
2885 'The LOST amountoutstanding is left intact' );
2886 # FIXME: Should we set the LOST fee status to 'FOUND' regardless of whether we're refunding or not?
2887 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2890 subtest 'lostreturn | refund' => sub {
2891 plan tests => 12;
2893 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund });
2895 my $item = $builder->build_sample_item(
2897 replacementprice => $replacement_amount
2901 # Issue the item
2902 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
2904 # Fake fines cronjob on this checkout
2905 my ($fine) =
2906 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2907 $ten_days_ago, $now );
2908 UpdateFine(
2910 issue_id => $issue->issue_id,
2911 itemnumber => $item->itemnumber,
2912 borrowernumber => $patron->borrowernumber,
2913 amount => $fine,
2914 due => output_pref($ten_days_ago)
2917 my $overdue_fees = Koha::Account::Lines->search(
2919 borrowernumber => $patron->id,
2920 itemnumber => $item->itemnumber,
2921 debit_type_code => 'OVERDUE'
2924 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
2925 my $overdue_fee = $overdue_fees->next;
2926 is( $overdue_fee->amount + 0,
2927 10, 'The right OVERDUE amount is generated' );
2928 is( $overdue_fee->amountoutstanding + 0,
2930 'The right OVERDUE amountoutstanding is generated' );
2932 # Simulate item marked as lost
2933 $item->itemlost(3)->store;
2934 C4::Circulation::LostItem( $item->itemnumber, 1 );
2936 my $lost_fee_lines = Koha::Account::Lines->search(
2938 borrowernumber => $patron->id,
2939 itemnumber => $item->itemnumber,
2940 debit_type_code => 'LOST'
2943 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
2944 my $lost_fee_line = $lost_fee_lines->next;
2945 is( $lost_fee_line->amount + 0,
2946 $replacement_amount, 'The right LOST amount is generated' );
2947 is( $lost_fee_line->amountoutstanding + 0,
2948 $replacement_amount,
2949 'The right LOST amountoutstanding is generated' );
2950 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
2952 # Return the lost item
2953 my ( undef, $message ) =
2954 AddReturn( $item->barcode, $branchcode_refund, undef, $five_days_ago );
2956 $overdue_fee->discard_changes;
2957 is( $overdue_fee->amount + 0,
2958 10, 'The OVERDUE amount is left intact' );
2959 is( $overdue_fee->amountoutstanding + 0,
2961 'The OVERDUE amountoutstanding is left intact' );
2963 $lost_fee_line->discard_changes;
2964 is( $lost_fee_line->amount + 0,
2965 $replacement_amount, 'The LOST amount is left intact' );
2966 is( $lost_fee_line->amountoutstanding + 0,
2968 'The LOST amountoutstanding is refunded' );
2969 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
2972 subtest 'lostreturn | restore' => sub {
2973 plan tests => 13;
2975 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_restore });
2977 my $item = $builder->build_sample_item(
2979 replacementprice => $replacement_amount
2983 # Issue the item
2984 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode , $ten_days_ago);
2986 # Fake fines cronjob on this checkout
2987 my ($fine) =
2988 CalcFine( $item, $patron->categorycode, $library->{branchcode},
2989 $ten_days_ago, $now );
2990 UpdateFine(
2992 issue_id => $issue->issue_id,
2993 itemnumber => $item->itemnumber,
2994 borrowernumber => $patron->borrowernumber,
2995 amount => $fine,
2996 due => output_pref($ten_days_ago)
2999 my $overdue_fees = Koha::Account::Lines->search(
3001 borrowernumber => $patron->id,
3002 itemnumber => $item->itemnumber,
3003 debit_type_code => 'OVERDUE'
3006 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3007 my $overdue_fee = $overdue_fees->next;
3008 is( $overdue_fee->amount + 0,
3009 10, 'The right OVERDUE amount is generated' );
3010 is( $overdue_fee->amountoutstanding + 0,
3012 'The right OVERDUE amountoutstanding is generated' );
3014 # Simulate item marked as lost
3015 $item->itemlost(3)->store;
3016 C4::Circulation::LostItem( $item->itemnumber, 1 );
3018 my $lost_fee_lines = Koha::Account::Lines->search(
3020 borrowernumber => $patron->id,
3021 itemnumber => $item->itemnumber,
3022 debit_type_code => 'LOST'
3025 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3026 my $lost_fee_line = $lost_fee_lines->next;
3027 is( $lost_fee_line->amount + 0,
3028 $replacement_amount, 'The right LOST amount is generated' );
3029 is( $lost_fee_line->amountoutstanding + 0,
3030 $replacement_amount,
3031 'The right LOST amountoutstanding is generated' );
3032 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3034 # Simulate refunding overdue fees upon marking item as lost
3035 my $overdue_forgive = $patron->account->add_credit(
3037 amount => 10.00,
3038 user_id => $manager->borrowernumber,
3039 library_id => $branchcode_restore,
3040 interface => 'test',
3041 type => 'FORGIVEN',
3042 item_id => $item->itemnumber
3045 $overdue_forgive->apply(
3046 { debits => [$overdue_fee], offset_type => 'Forgiven' } );
3047 $overdue_fee->discard_changes;
3048 is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
3050 # Do nothing
3051 my ( undef, $message ) =
3052 AddReturn( $item->barcode, $branchcode_restore, undef, $five_days_ago );
3054 $overdue_fee->discard_changes;
3055 is( $overdue_fee->amount + 0,
3056 10, 'The OVERDUE amount is left intact' );
3057 is( $overdue_fee->amountoutstanding + 0,
3059 'The OVERDUE amountoutstanding is restored' );
3061 $lost_fee_line->discard_changes;
3062 is( $lost_fee_line->amount + 0,
3063 $replacement_amount, 'The LOST amount is left intact' );
3064 is( $lost_fee_line->amountoutstanding + 0,
3066 'The LOST amountoutstanding is refunded' );
3067 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3070 subtest 'lostreturn | charge' => sub {
3071 plan tests => 16;
3073 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_charge });
3075 my $item = $builder->build_sample_item(
3077 replacementprice => $replacement_amount
3081 # Issue the item
3082 my $issue = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode, $ten_days_ago );
3084 # Fake fines cronjob on this checkout
3085 my ($fine) =
3086 CalcFine( $item, $patron->categorycode, $library->{branchcode},
3087 $ten_days_ago, $now );
3088 UpdateFine(
3090 issue_id => $issue->issue_id,
3091 itemnumber => $item->itemnumber,
3092 borrowernumber => $patron->borrowernumber,
3093 amount => $fine,
3094 due => output_pref($ten_days_ago)
3097 my $overdue_fees = Koha::Account::Lines->search(
3099 borrowernumber => $patron->id,
3100 itemnumber => $item->itemnumber,
3101 debit_type_code => 'OVERDUE'
3104 is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3105 my $overdue_fee = $overdue_fees->next;
3106 is( $overdue_fee->amount + 0,
3107 10, 'The right OVERDUE amount is generated' );
3108 is( $overdue_fee->amountoutstanding + 0,
3110 'The right OVERDUE amountoutstanding is generated' );
3112 # Simulate item marked as lost
3113 $item->itemlost(3)->store;
3114 C4::Circulation::LostItem( $item->itemnumber, 1 );
3116 my $lost_fee_lines = Koha::Account::Lines->search(
3118 borrowernumber => $patron->id,
3119 itemnumber => $item->itemnumber,
3120 debit_type_code => 'LOST'
3123 is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3124 my $lost_fee_line = $lost_fee_lines->next;
3125 is( $lost_fee_line->amount + 0,
3126 $replacement_amount, 'The right LOST amount is generated' );
3127 is( $lost_fee_line->amountoutstanding + 0,
3128 $replacement_amount,
3129 'The right LOST amountoutstanding is generated' );
3130 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3132 # Simulate refunding overdue fees upon marking item as lost
3133 my $overdue_forgive = $patron->account->add_credit(
3135 amount => 10.00,
3136 user_id => $manager->borrowernumber,
3137 library_id => $branchcode_charge,
3138 interface => 'test',
3139 type => 'FORGIVEN',
3140 item_id => $item->itemnumber
3143 $overdue_forgive->apply(
3144 { debits => [$overdue_fee], offset_type => 'Forgiven' } );
3145 $overdue_fee->discard_changes;
3146 is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
3148 # Do nothing
3149 my ( undef, $message ) =
3150 AddReturn( $item->barcode, $branchcode_charge, undef, $five_days_ago );
3152 $lost_fee_line->discard_changes;
3153 is( $lost_fee_line->amount + 0,
3154 $replacement_amount, 'The LOST amount is left intact' );
3155 is( $lost_fee_line->amountoutstanding + 0,
3157 'The LOST amountoutstanding is refunded' );
3158 is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
3160 $overdue_fees = Koha::Account::Lines->search(
3162 borrowernumber => $patron->id,
3163 itemnumber => $item->itemnumber,
3164 debit_type_code => 'OVERDUE'
3167 order_by => { '-asc' => 'accountlines_id'}
3170 is( $overdue_fees->count, 2, 'A second OVERDUE fee has been added' );
3171 $overdue_fee = $overdue_fees->next;
3172 is( $overdue_fee->amount + 0,
3173 10, 'The original OVERDUE amount is left intact' );
3174 is( $overdue_fee->amountoutstanding + 0,
3176 'The original OVERDUE amountoutstanding is left as forgiven' );
3177 $overdue_fee = $overdue_fees->next;
3178 is( $overdue_fee->amount + 0,
3179 5, 'The new OVERDUE amount is correct for the backdated return' );
3180 is( $overdue_fee->amountoutstanding + 0,
3182 'The new OVERDUE amountoutstanding is correct for the backdated return' );
3187 subtest '_FixOverduesOnReturn' => sub {
3188 plan tests => 14;
3190 my $manager = $builder->build_object({ class => "Koha::Patrons" });
3191 t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3193 my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
3195 my $branchcode = $library2->{branchcode};
3197 my $item = $builder->build_sample_item(
3199 biblionumber => $biblio->biblionumber,
3200 library => $branchcode,
3201 replacementprice => 99.00,
3202 itype => $itemtype,
3206 my $patron = $builder->build( { source => 'Borrower' } );
3208 ## Start with basic call, should just close out the open fine
3209 my $accountline = Koha::Account::Line->new(
3211 borrowernumber => $patron->{borrowernumber},
3212 debit_type_code => 'OVERDUE',
3213 status => 'UNRETURNED',
3214 itemnumber => $item->itemnumber,
3215 amount => 99.00,
3216 amountoutstanding => 99.00,
3217 interface => 'test',
3219 )->store();
3221 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
3223 $accountline->_result()->discard_changes();
3225 is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
3226 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3227 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3229 ## Run again, with exemptfine enabled
3230 $accountline->set(
3232 debit_type_code => 'OVERDUE',
3233 status => 'UNRETURNED',
3234 amountoutstanding => 99.00,
3236 )->store();
3238 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3240 $accountline->_result()->discard_changes();
3241 my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3243 is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
3244 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3245 is( $accountline->status, 'RETURNED', 'Open fine ( account type OVERDUE ) has been set to returned ( status RETURNED )');
3246 is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
3247 is( $offset->amount + 0, -99, "Amount of offset is correct" );
3248 my $credit = $offset->credit;
3249 is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
3250 is( $credit->amount + 0, -99, "Credit amount is set correctly" );
3251 is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
3253 # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
3254 $accountline->set(
3256 debit_type_code => 'OVERDUE',
3257 status => 'UNRETURNED',
3258 amountoutstanding => 0.00,
3260 )->store();
3261 $offset->delete;
3263 C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3265 $accountline->_result()->discard_changes();
3266 $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3267 is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
3268 isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3269 is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3272 subtest 'Set waiting flag' => sub {
3273 plan tests => 11;
3275 my $library_1 = $builder->build( { source => 'Branch' } );
3276 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3277 my $library_2 = $builder->build( { source => 'Branch' } );
3278 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3280 my $item = $builder->build_sample_item(
3282 library => $library_1->{branchcode},
3286 set_userenv( $library_2 );
3287 my $reserve_id = AddReserve(
3289 branchcode => $library_2->{branchcode},
3290 borrowernumber => $patron_2->{borrowernumber},
3291 biblionumber => $item->biblionumber,
3292 priority => 1,
3293 itemnumber => $item->itemnumber,
3297 set_userenv( $library_1 );
3298 my $do_transfer = 1;
3299 my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3300 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3301 my $hold = Koha::Holds->find( $reserve_id );
3302 is( $hold->found, 'T', 'Hold is in transit' );
3304 my ( $status ) = CheckReserves($item->itemnumber);
3305 is( $status, 'Reserved', 'Hold is not waiting yet');
3307 set_userenv( $library_2 );
3308 $do_transfer = 0;
3309 AddReturn( $item->barcode, $library_2->{branchcode} );
3310 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3311 $hold = Koha::Holds->find( $reserve_id );
3312 is( $hold->found, 'W', 'Hold is waiting' );
3313 ( $status ) = CheckReserves($item->itemnumber);
3314 is( $status, 'Waiting', 'Now the hold is waiting');
3316 #Bug 21944 - Waiting transfer checked in at branch other than pickup location
3317 set_userenv( $library_1 );
3318 (undef, my $messages, undef, undef ) = AddReturn ( $item->barcode, $library_1->{branchcode} );
3319 $hold = Koha::Holds->find( $reserve_id );
3320 is( $hold->found, undef, 'Hold is no longer marked waiting' );
3321 is( $hold->priority, 1, "Hold is now priority one again");
3322 is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
3323 is( $hold->itemnumber, $item->itemnumber, "Hold has retained its' itemnumber");
3324 is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
3325 is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
3326 is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
3329 subtest 'Cancel transfers on lost items' => sub {
3330 plan tests => 6;
3331 my $library_1 = $builder->build( { source => 'Branch' } );
3332 my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3333 my $library_2 = $builder->build( { source => 'Branch' } );
3334 my $patron_2 = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3335 my $biblio = $builder->build_sample_biblio({branchcode => $library->{branchcode}});
3336 my $item = $builder->build_sample_item({
3337 biblionumber => $biblio->biblionumber,
3338 library => $library_1->{branchcode},
3341 set_userenv( $library_2 );
3342 my $reserve_id = AddReserve(
3344 branchcode => $library_2->{branchcode},
3345 borrowernumber => $patron_2->{borrowernumber},
3346 biblionumber => $item->biblionumber,
3347 priority => 1,
3348 itemnumber => $item->itemnumber,
3352 #Return book and add transfer
3353 set_userenv( $library_1 );
3354 my $do_transfer = 1;
3355 my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3356 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3357 C4::Circulation::transferbook({
3358 from_branch => $library_1->{branchcode},
3359 to_branch => $library_2->{branchcode},
3360 barcode => $item->barcode,
3362 my $hold = Koha::Holds->find( $reserve_id );
3363 is( $hold->found, 'T', 'Hold is in transit' );
3365 #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
3366 my ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3367 is( $frombranch, $library_1->{branchcode}, 'The transfer is generated from the correct library');
3368 is( $tobranch, $library_2->{branchcode}, 'The transfer is generated to the correct library');
3369 my $itemcheck = Koha::Items->find($item->itemnumber);
3370 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Items holding branch is the transfers origination branch before it is marked as lost' );
3372 #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
3373 $item->itemlost(1)->store;
3374 LostItem( $item->itemnumber, 'test', 1 );
3375 ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3376 is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
3377 $itemcheck = Koha::Items->find($item->itemnumber);
3378 is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
3382 subtest 'CanBookBeIssued | is_overdue' => sub {
3383 plan tests => 3;
3385 # Set a simple circ policy
3386 Koha::CirculationRules->set_rules(
3388 categorycode => undef,
3389 branchcode => undef,
3390 itemtype => undef,
3391 rules => {
3392 maxissueqty => 1,
3393 reservesallowed => 25,
3394 issuelength => 14,
3395 lengthunit => 'days',
3396 renewalsallowed => 1,
3397 renewalperiod => 7,
3398 norenewalbefore => undef,
3399 auto_renew => 0,
3400 fine => .10,
3401 chargeperiod => 1,
3406 my $now = dt_from_string;
3407 my $five_days_go = output_pref({ dt => $now->clone->add( days => 5 ), dateonly => 1});
3408 my $ten_days_go = output_pref({ dt => $now->clone->add( days => 10), dateonly => 1 });
3409 my $library = $builder->build( { source => 'Branch' } );
3410 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
3412 my $item = $builder->build_sample_item(
3414 library => $library->{branchcode},
3418 my $issue = AddIssue( $patron->unblessed, $item->barcode, $five_days_go ); # date due was 10d ago
3419 my $actualissue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
3420 is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
3421 my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->barcode,$ten_days_go, undef, undef, undef);
3422 is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
3423 is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
3426 subtest 'ItemsDeniedRenewal preference' => sub {
3427 plan tests => 18;
3429 C4::Context->set_preference('ItemsDeniedRenewal','');
3431 my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
3432 Koha::CirculationRules->set_rules(
3434 categorycode => '*',
3435 itemtype => '*',
3436 branchcode => $idr_lib->branchcode,
3437 rules => {
3438 reservesallowed => 25,
3439 issuelength => 14,
3440 lengthunit => 'days',
3441 renewalsallowed => 10,
3442 renewalperiod => 7,
3443 norenewalbefore => undef,
3444 auto_renew => 0,
3445 fine => .10,
3446 chargeperiod => 1,
3451 my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
3452 homebranch => $idr_lib->branchcode,
3453 withdrawn => 1,
3454 itype => 'HIDE',
3455 location => 'PROC',
3456 itemcallnumber => undef,
3457 itemnotes => "",
3460 my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
3461 homebranch => $idr_lib->branchcode,
3462 withdrawn => 0,
3463 itype => 'NOHIDE',
3464 location => 'NOPROC'
3468 my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
3469 branchcode => $idr_lib->branchcode,
3472 my $future = dt_from_string->add( days => 1 );
3473 my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3474 returndate => undef,
3475 renewals => 0,
3476 auto_renew => 0,
3477 borrowernumber => $idr_borrower->borrowernumber,
3478 itemnumber => $deny_book->itemnumber,
3479 onsite_checkout => 0,
3480 date_due => $future,
3483 my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3484 returndate => undef,
3485 renewals => 0,
3486 auto_renew => 0,
3487 borrowernumber => $idr_borrower->borrowernumber,
3488 itemnumber => $allow_book->itemnumber,
3489 onsite_checkout => 0,
3490 date_due => $future,
3494 my $idr_rules;
3496 my ( $idr_mayrenew, $idr_error ) =
3497 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3498 is( $idr_mayrenew, 1, 'Renewal allowed when no rules' );
3499 is( $idr_error, undef, 'Renewal allowed when no rules' );
3501 $idr_rules="withdrawn: [1]";
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 when 1 rules (withdrawn)' );
3507 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 1 rule (withdrawn)' );
3508 ( $idr_mayrenew, $idr_error ) =
3509 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3510 is( $idr_mayrenew, 1, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3511 is( $idr_error, undef, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3513 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
3515 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3516 ( $idr_mayrenew, $idr_error ) =
3517 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3518 is( $idr_mayrenew, 0, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
3519 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 2 rules matched (withdrawn,itype)' );
3520 ( $idr_mayrenew, $idr_error ) =
3521 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3522 is( $idr_mayrenew, 1, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3523 is( $idr_error, undef, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3525 $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
3527 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3528 ( $idr_mayrenew, $idr_error ) =
3529 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3530 is( $idr_mayrenew, 0, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
3531 is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 3 rules matched (withdrawn,itype, location)' );
3532 ( $idr_mayrenew, $idr_error ) =
3533 CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3534 is( $idr_mayrenew, 1, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3535 is( $idr_error, undef, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3537 $idr_rules="itemcallnumber: [NULL]";
3538 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3539 ( $idr_mayrenew, $idr_error ) =
3540 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3541 is( $idr_mayrenew, 0, 'Renewal blocked for undef when NULL in pref' );
3542 $idr_rules="itemcallnumber: ['']";
3543 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3544 ( $idr_mayrenew, $idr_error ) =
3545 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3546 is( $idr_mayrenew, 1, 'Renewal not blocked for undef when "" in pref' );
3548 $idr_rules="itemnotes: [NULL]";
3549 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3550 ( $idr_mayrenew, $idr_error ) =
3551 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3552 is( $idr_mayrenew, 1, 'Renewal not blocked for "" when NULL in pref' );
3553 $idr_rules="itemnotes: ['']";
3554 C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3555 ( $idr_mayrenew, $idr_error ) =
3556 CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3557 is( $idr_mayrenew, 0, 'Renewal blocked for empty string when "" in pref' );
3560 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
3561 plan tests => 2;
3563 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3564 my $library = $builder->build( { source => 'Branch' } );
3565 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3567 my $item = $builder->build_sample_item(
3569 library => $library->{branchcode},
3573 my ( $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' );
3578 subtest 'CanBookBeIssued | notforloan' => sub {
3579 plan tests => 2;
3581 t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
3583 my $library = $builder->build( { source => 'Branch' } );
3584 my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3586 my $itemtype = $builder->build(
3588 source => 'Itemtype',
3589 value => { notforloan => undef, }
3592 my $item = $builder->build_sample_item(
3594 library => $library->{branchcode},
3595 itype => $itemtype->{itemtype},
3598 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3600 my ( $issuingimpossible, $needsconfirmation );
3603 subtest 'item-level_itypes = 1' => sub {
3604 plan tests => 6;
3606 t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
3607 # Is for loan at item type and item level
3608 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3609 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3610 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3612 # not for loan at item type level
3613 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3614 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3615 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3616 is_deeply(
3617 $issuingimpossible,
3618 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3619 'Item can not be issued, not for loan at item type level'
3622 # not for loan at item level
3623 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3624 $item->notforloan( 1 )->store;
3625 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3626 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3627 is_deeply(
3628 $issuingimpossible,
3629 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3630 'Item can not be issued, not for loan at item type level'
3634 subtest 'item-level_itypes = 0' => sub {
3635 plan tests => 6;
3637 t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3639 # We set another itemtype for biblioitem
3640 my $itemtype = $builder->build(
3642 source => 'Itemtype',
3643 value => { notforloan => undef, }
3647 # for loan at item type and item level
3648 $item->notforloan(0)->store;
3649 $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3650 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3651 is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3652 is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3654 # not for loan at item type level
3655 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3656 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3657 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3658 is_deeply(
3659 $issuingimpossible,
3660 { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3661 'Item can not be issued, not for loan at item type level'
3664 # not for loan at item level
3665 Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3666 $item->notforloan( 1 )->store;
3667 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3668 is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3669 is_deeply(
3670 $issuingimpossible,
3671 { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3672 'Item can not be issued, not for loan at item type level'
3676 # TODO test with AllowNotForLoanOverride = 1
3679 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
3680 plan tests => 1;
3682 t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
3683 my $item = $builder->build_sample_item(
3685 onloan => '2018-01-01',
3689 AddReturn( $item->barcode, $item->homebranch );
3690 $item->discard_changes; # refresh
3691 is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
3695 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
3697 plan tests => 12;
3700 t::lib::Mocks::mock_preference('item-level_itypes', 1);
3702 my $issuing_charges = 15;
3703 my $title = 'A title';
3704 my $author = 'Author, An';
3705 my $barcode = 'WHATARETHEODDS';
3707 my $circ = Test::MockModule->new('C4::Circulation');
3708 $circ->mock(
3709 'GetIssuingCharges',
3710 sub {
3711 return $issuing_charges;
3715 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3716 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
3717 my $patron = $builder->build_object({
3718 class => 'Koha::Patrons',
3719 value => { branchcode => $library->id }
3722 my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
3723 my $item_id = Koha::Item->new(
3725 biblionumber => $biblio->biblionumber,
3726 homebranch => $library->id,
3727 holdingbranch => $library->id,
3728 barcode => $barcode,
3729 replacementprice => 23.00,
3730 itype => $itemtype->id
3732 )->store->itemnumber;
3733 my $item = Koha::Items->find( $item_id );
3735 my $context = Test::MockModule->new('C4::Context');
3736 $context->mock( userenv => { branch => $library->id } );
3738 # Check the item out
3739 AddIssue( $patron->unblessed, $item->barcode );
3740 t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
3741 my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
3742 my %params_renewal = (
3743 timestamp => { -like => $date . "%" },
3744 module => "CIRCULATION",
3745 action => "RENEWAL",
3747 my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
3748 AddRenewal( $patron->id, $item->id, $library->id );
3749 my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3750 is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
3752 my $checkouts = $patron->checkouts;
3753 # The following will fail if run on 00:00:00
3754 unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
3756 my $lines = Koha::Account::Lines->search({
3757 borrowernumber => $patron->id,
3758 itemnumber => $item->id
3761 is( $lines->count, 2 );
3763 my $line = $lines->next;
3764 is( $line->debit_type_code, 'RENT', 'The issue of item with issuing charge generates an accountline of the correct type' );
3765 is( $line->branchcode, $library->id, 'AddIssuingCharge correctly sets branchcode' );
3766 is( $line->description, '', 'AddIssue does not set a hardcoded description for the accountline' );
3768 $line = $lines->next;
3769 is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
3770 is( $line->branchcode, $library->id, 'AddRenewal correctly sets branchcode' );
3771 is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
3773 t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
3775 $context = Test::MockModule->new('C4::Context');
3776 $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
3778 my $now = dt_from_string;
3779 $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
3780 $old_log_size = Koha::ActionLogs->count( \%params_renewal );
3781 my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
3782 $sth->execute($item->id, $library->id);
3783 my ($old_stats_size) = $sth->fetchrow_array;
3784 AddRenewal( $patron->id, $item->id, $library->id );
3785 $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3786 $sth->execute($item->id, $library->id);
3787 my ($new_stats_size) = $sth->fetchrow_array;
3788 is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
3789 is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
3791 AddReturn( $item->id, $library->id, undef, $date );
3792 AddIssue( $patron->unblessed, $item->barcode, $now );
3793 AddRenewal( $patron->id, $item->id, $library->id, undef, undef, 1 );
3794 my $lines_skipped = Koha::Account::Lines->search({
3795 borrowernumber => $patron->id,
3796 itemnumber => $item->id
3798 is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
3802 subtest 'ProcessOfflinePayment() tests' => sub {
3804 plan tests => 4;
3807 my $amount = 123;
3809 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
3810 my $library = $builder->build_object({ class => 'Koha::Libraries' });
3811 my $result = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
3813 is( $result, 'Success.', 'The right string is returned' );
3815 my $lines = $patron->account->lines;
3816 is( $lines->count, 1, 'line created correctly');
3818 my $line = $lines->next;
3819 is( $line->amount+0, $amount * -1, 'amount picked from params' );
3820 is( $line->branchcode, $library->id, 'branchcode set correctly' );
3824 subtest 'Incremented fee tests' => sub {
3825 plan tests => 19;
3827 my $dt = dt_from_string();
3828 Time::Fake->offset( $dt->epoch );
3830 t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
3832 my $library =
3833 $builder->build_object( { class => 'Koha::Libraries' } )->store;
3835 $module->mock( 'userenv', sub { { branch => $library->id } } );
3837 my $patron = $builder->build_object(
3839 class => 'Koha::Patrons',
3840 value => { categorycode => $patron_category->{categorycode} }
3842 )->store;
3844 my $itemtype = $builder->build_object(
3846 class => 'Koha::ItemTypes',
3847 value => {
3848 notforloan => undef,
3849 rentalcharge => 0,
3850 rentalcharge_daily => 1,
3851 rentalcharge_daily_calendar => 0
3854 )->store;
3856 my $item = $builder->build_sample_item(
3858 library => $library->{branchcode},
3859 itype => $itemtype->id,
3863 is( $itemtype->rentalcharge_daily+0,
3864 1, 'Daily rental charge stored and retreived correctly' );
3865 is( $item->effective_itemtype, $itemtype->id,
3866 "Itemtype set correctly for item" );
3868 my $now = dt_from_string;
3869 my $dt_from = $now->clone;
3870 my $dt_to = $now->clone->add( days => 7 );
3871 my $dt_to_renew = $now->clone->add( days => 13 );
3873 # Daily Tests
3874 my $issue =
3875 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3876 my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3877 is( $accountline->amount+0, 7,
3878 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
3880 $accountline->delete();
3881 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3882 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3883 is( $accountline->amount+0, 6,
3884 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
3886 $accountline->delete();
3887 $issue->delete();
3889 t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
3890 $itemtype->rentalcharge_daily_calendar(1)->store();
3891 $issue =
3892 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3893 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3894 is( $accountline->amount+0, 7,
3895 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
3897 $accountline->delete();
3898 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3899 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3900 is( $accountline->amount+0, 6,
3901 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
3903 $accountline->delete();
3904 $issue->delete();
3906 my $calendar = C4::Calendar->new( branchcode => $library->id );
3907 # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
3908 my $closed_day =
3909 ( $dt_from->day_of_week == 6 ) ? 0
3910 : ( $dt_from->day_of_week == 7 ) ? 1
3911 : $dt_from->day_of_week + 1;
3912 my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
3913 $calendar->insert_week_day_holiday(
3914 weekday => $closed_day,
3915 title => 'Test holiday',
3916 description => 'Test holiday'
3918 $issue =
3919 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3920 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3921 is( $accountline->amount+0, 6,
3922 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
3924 $accountline->delete();
3925 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3926 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3927 is( $accountline->amount+0, 5,
3928 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
3930 $accountline->delete();
3931 $issue->delete();
3933 $itemtype->rentalcharge(2)->store;
3934 is( $itemtype->rentalcharge+0, 2,
3935 'Rental charge updated and retreived correctly' );
3936 $issue =
3937 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3938 my $accountlines =
3939 Koha::Account::Lines->search( { itemnumber => $item->id } );
3940 is( $accountlines->count, '2',
3941 "Fixed charge and accrued charge recorded distinctly" );
3942 $accountlines->delete();
3943 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3944 $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
3945 is( $accountlines->count, '2',
3946 "Fixed charge and accrued charge recorded distinctly, for renewal" );
3947 $accountlines->delete();
3948 $issue->delete();
3949 $itemtype->rentalcharge(0)->store;
3950 is( $itemtype->rentalcharge+0, 0,
3951 'Rental charge reset and retreived correctly' );
3953 # Hourly
3954 Koha::CirculationRules->set_rule(
3956 categorycode => $patron->categorycode,
3957 itemtype => $itemtype->id,
3958 branchcode => $library->id,
3959 rule_name => 'lengthunit',
3960 rule_value => 'hours',
3964 $itemtype->rentalcharge_hourly('0.25')->store();
3965 is( $itemtype->rentalcharge_hourly,
3966 '0.25', 'Hourly rental charge stored and retreived correctly' );
3968 $dt_to = $now->clone->add( hours => 168 );
3969 $dt_to_renew = $now->clone->add( hours => 312 );
3971 $itemtype->rentalcharge_hourly_calendar(0)->store();
3972 $issue =
3973 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3974 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3975 is( $accountline->amount + 0, 42,
3976 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)" );
3977 $accountline->delete();
3978 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3979 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3980 is( $accountline->amount + 0, 36,
3981 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)" );
3982 $accountline->delete();
3983 $issue->delete();
3985 $itemtype->rentalcharge_hourly_calendar(1)->store();
3986 $issue =
3987 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3988 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3989 is( $accountline->amount + 0, 36,
3990 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)" );
3991 $accountline->delete();
3992 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3993 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3994 is( $accountline->amount + 0, 30,
3995 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u" );
3996 $accountline->delete();
3997 $issue->delete();
3999 $calendar->delete_holiday( weekday => $closed_day );
4000 $issue =
4001 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4002 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4003 is( $accountline->amount + 0, 42,
4004 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u" );
4005 $accountline->delete();
4006 AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
4007 $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
4008 is( $accountline->amount + 0, 36,
4009 "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)" );
4010 $accountline->delete();
4011 $issue->delete();
4012 Time::Fake->reset;
4015 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
4016 plan tests => 2;
4018 t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
4019 t::lib::Mocks::mock_preference('item-level_itypes', 1);
4021 my $library =
4022 $builder->build_object( { class => 'Koha::Libraries' } )->store;
4023 my $patron = $builder->build_object(
4025 class => 'Koha::Patrons',
4026 value => { categorycode => $patron_category->{categorycode} }
4028 )->store;
4030 my $itemtype = $builder->build_object(
4032 class => 'Koha::ItemTypes',
4033 value => {
4034 notforloan => 0,
4035 rentalcharge => 0,
4036 rentalcharge_daily => 0
4041 my $item = $builder->build_sample_item(
4043 library => $library->id,
4044 notforloan => 0,
4045 itemlost => 0,
4046 withdrawn => 0,
4047 itype => $itemtype->id,
4049 )->store;
4051 my ( $issuingimpossible, $needsconfirmation );
4052 my $dt_from = dt_from_string();
4053 my $dt_due = $dt_from->clone->add( days => 3 );
4055 $itemtype->rentalcharge(1)->store;
4056 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4057 is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
4058 $itemtype->rentalcharge('0')->store;
4059 $itemtype->rentalcharge_daily(1)->store;
4060 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4061 is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
4062 $itemtype->rentalcharge_daily('0')->store;
4065 subtest 'CanBookBeIssued & CircConfirmItemParts' => sub {
4066 plan tests => 1;
4068 t::lib::Mocks::mock_preference('CircConfirmItemParts', 1);
4070 my $patron = $builder->build_object(
4072 class => 'Koha::Patrons',
4073 value => { categorycode => $patron_category->{categorycode} }
4075 )->store;
4077 my $item = $builder->build_sample_item(
4079 materials => 'includes DVD',
4081 )->store;
4083 my $dt_due = dt_from_string->add( days => 3 );
4085 my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
4086 is_deeply( $needsconfirmation, { ADDITIONAL_MATERIALS => 'includes DVD' }, 'Item needs confirmation of additional parts' );
4089 subtest 'Do not return on renewal (LOST charge)' => sub {
4090 plan tests => 1;
4092 t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
4093 my $library = $builder->build_object( { class => "Koha::Libraries" } );
4094 my $manager = $builder->build_object( { class => "Koha::Patrons" } );
4095 t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
4097 my $biblio = $builder->build_sample_biblio;
4099 my $item = $builder->build_sample_item(
4101 biblionumber => $biblio->biblionumber,
4102 library => $library->branchcode,
4103 replacementprice => 99.00,
4104 itype => $itemtype,
4108 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
4109 AddIssue( $patron->unblessed, $item->barcode );
4111 my $accountline = Koha::Account::Line->new(
4113 borrowernumber => $patron->borrowernumber,
4114 debit_type_code => 'LOST',
4115 status => undef,
4116 itemnumber => $item->itemnumber,
4117 amount => 12,
4118 amountoutstanding => 12,
4119 interface => 'something',
4121 )->store();
4123 # AddRenewal doesn't call _FixAccountForLostAndFound
4124 AddIssue( $patron->unblessed, $item->barcode );
4126 is( $patron->checkouts->count, 1,
4127 'Renewal should not return the item even if a LOST payment has been made earlier'
4131 subtest 'Filling a hold should cancel existing transfer' => sub {
4132 plan tests => 4;
4134 t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
4136 my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
4137 my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
4138 my $patron = $builder->build_object(
4140 class => 'Koha::Patrons',
4141 value => {
4142 categorycode => $patron_category->{categorycode},
4143 branchcode => $libraryA->branchcode,
4146 )->store;
4148 my $item = $builder->build_sample_item({
4149 homebranch => $libraryB->branchcode,
4152 my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4153 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
4154 AddReserve({
4155 branchcode => $libraryA->branchcode,
4156 borrowernumber => $patron->borrowernumber,
4157 biblionumber => $item->biblionumber,
4158 itemnumber => $item->itemnumber
4160 my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
4161 is( $reserves->count, 1, "Reserve is placed");
4162 ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4163 my $reserve = $reserves->next;
4164 ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
4165 $reserve->discard_changes;
4166 ok( $reserve->found eq 'W', "Reserve is marked waiting" );
4167 is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
4170 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddReturn' => sub {
4172 plan tests => 4;
4174 t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
4175 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4176 my $patron = $builder->build_object(
4178 class => 'Koha::Patrons',
4179 value => { categorycode => $patron_category->{categorycode} }
4183 my $biblionumber = $builder->build_sample_biblio(
4185 branchcode => $library->branchcode,
4187 )->biblionumber;
4189 # And the circulation rule
4190 Koha::CirculationRules->search->delete;
4191 Koha::CirculationRules->set_rules(
4193 categorycode => undef,
4194 itemtype => undef,
4195 branchcode => undef,
4196 rules => {
4197 issuelength => 14,
4198 lengthunit => 'days',
4202 $builder->build(
4204 source => 'CirculationRule',
4205 value => {
4206 branchcode => undef,
4207 categorycode => undef,
4208 itemtype => undef,
4209 rule_name => 'lostreturn',
4210 rule_value => 'refund'
4215 subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
4216 plan tests => 3;
4218 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4219 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
4221 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4223 my $item = $builder->build_sample_item(
4225 biblionumber => $biblionumber,
4226 library => $library->branchcode,
4227 replacementprice => '42',
4230 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4231 LostItem( $item->itemnumber, 'cli', 0 );
4232 $item->_result->itemlost(1);
4233 $item->_result->itemlost_on( $lost_on );
4234 $item->_result->update();
4236 my $a = Koha::Account::Lines->search(
4238 itemnumber => $item->id,
4239 borrowernumber => $patron->borrowernumber
4241 )->next;
4242 ok( $a, "Found accountline for lost fee" );
4243 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4244 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4245 $a = $a->get_from_storage;
4246 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4247 $a->delete;
4250 subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
4251 plan tests => 3;
4253 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4254 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4256 my $lost_on = dt_from_string->subtract( days => 6 )->date;
4258 my $item = $builder->build_sample_item(
4260 biblionumber => $biblionumber,
4261 library => $library->branchcode,
4262 replacementprice => '42',
4265 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4266 LostItem( $item->itemnumber, 'cli', 0 );
4267 $item->_result->itemlost(1);
4268 $item->_result->itemlost_on( $lost_on );
4269 $item->_result->update();
4271 my $a = Koha::Account::Lines->search(
4273 itemnumber => $item->id,
4274 borrowernumber => $patron->borrowernumber
4276 )->next;
4277 ok( $a, "Found accountline for lost fee" );
4278 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4279 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4280 $a = $a->get_from_storage;
4281 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4282 $a->delete;
4285 subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
4286 plan tests => 3;
4288 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4289 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4291 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4293 my $item = $builder->build_sample_item(
4295 biblionumber => $biblionumber,
4296 library => $library->branchcode,
4297 replacementprice => '42',
4300 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4301 LostItem( $item->itemnumber, 'cli', 0 );
4302 $item->_result->itemlost(1);
4303 $item->_result->itemlost_on( $lost_on );
4304 $item->_result->update();
4306 my $a = Koha::Account::Lines->search(
4308 itemnumber => $item->id,
4309 borrowernumber => $patron->borrowernumber
4311 )->next;
4312 ok( $a, "Found accountline for lost fee" );
4313 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4314 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4315 $a = $a->get_from_storage;
4316 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4317 $a->delete;
4320 subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
4321 plan tests => 3;
4323 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4324 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4326 my $lost_on = dt_from_string->subtract( days => 8 )->date;
4328 my $item = $builder->build_sample_item(
4330 biblionumber => $biblionumber,
4331 library => $library->branchcode,
4332 replacementprice => '42',
4335 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4336 LostItem( $item->itemnumber, 'cli', 0 );
4337 $item->_result->itemlost(1);
4338 $item->_result->itemlost_on( $lost_on );
4339 $item->_result->update();
4341 my $a = Koha::Account::Lines->search(
4343 itemnumber => $item->id,
4344 borrowernumber => $patron->borrowernumber
4347 $a = $a->next;
4348 ok( $a, "Found accountline for lost fee" );
4349 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4350 my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
4351 $a = $a->get_from_storage;
4352 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4353 $a->delete;
4357 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddIssue' => sub {
4359 plan tests => 4;
4361 t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
4362 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4363 my $patron = $builder->build_object(
4365 class => 'Koha::Patrons',
4366 value => { categorycode => $patron_category->{categorycode} }
4369 my $patron2 = $builder->build_object(
4371 class => 'Koha::Patrons',
4372 value => { categorycode => $patron_category->{categorycode} }
4376 my $biblionumber = $builder->build_sample_biblio(
4378 branchcode => $library->branchcode,
4380 )->biblionumber;
4382 # And the circulation rule
4383 Koha::CirculationRules->search->delete;
4384 Koha::CirculationRules->set_rules(
4386 categorycode => undef,
4387 itemtype => undef,
4388 branchcode => undef,
4389 rules => {
4390 issuelength => 14,
4391 lengthunit => 'days',
4395 $builder->build(
4397 source => 'CirculationRule',
4398 value => {
4399 branchcode => undef,
4400 categorycode => undef,
4401 itemtype => undef,
4402 rule_name => 'lostreturn',
4403 rule_value => 'refund'
4408 subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
4409 plan tests => 3;
4411 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4412 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
4414 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4416 my $item = $builder->build_sample_item(
4418 biblionumber => $biblionumber,
4419 library => $library->branchcode,
4420 replacementprice => '42',
4423 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4424 LostItem( $item->itemnumber, 'cli', 0 );
4425 $item->_result->itemlost(1);
4426 $item->_result->itemlost_on( $lost_on );
4427 $item->_result->update();
4429 my $a = Koha::Account::Lines->search(
4431 itemnumber => $item->id,
4432 borrowernumber => $patron->borrowernumber
4434 )->next;
4435 ok( $a, "Found accountline for lost fee" );
4436 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4437 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4438 $a = $a->get_from_storage;
4439 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4440 $a->delete;
4441 $issue->delete;
4444 subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
4445 plan tests => 3;
4447 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4448 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4450 my $lost_on = dt_from_string->subtract( days => 6 )->date;
4452 my $item = $builder->build_sample_item(
4454 biblionumber => $biblionumber,
4455 library => $library->branchcode,
4456 replacementprice => '42',
4459 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4460 LostItem( $item->itemnumber, 'cli', 0 );
4461 $item->_result->itemlost(1);
4462 $item->_result->itemlost_on( $lost_on );
4463 $item->_result->update();
4465 my $a = Koha::Account::Lines->search(
4467 itemnumber => $item->id,
4468 borrowernumber => $patron->borrowernumber
4470 )->next;
4471 ok( $a, "Found accountline for lost fee" );
4472 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4473 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4474 $a = $a->get_from_storage;
4475 is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
4476 $a->delete;
4479 subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
4480 plan tests => 3;
4482 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4483 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4485 my $lost_on = dt_from_string->subtract( days => 7 )->date;
4487 my $item = $builder->build_sample_item(
4489 biblionumber => $biblionumber,
4490 library => $library->branchcode,
4491 replacementprice => '42',
4494 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4495 LostItem( $item->itemnumber, 'cli', 0 );
4496 $item->_result->itemlost(1);
4497 $item->_result->itemlost_on( $lost_on );
4498 $item->_result->update();
4500 my $a = Koha::Account::Lines->search(
4502 itemnumber => $item->id,
4503 borrowernumber => $patron->borrowernumber
4505 )->next;
4506 ok( $a, "Found accountline for lost fee" );
4507 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4508 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4509 $a = $a->get_from_storage;
4510 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4511 $a->delete;
4514 subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
4515 plan tests => 3;
4517 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
4518 t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
4520 my $lost_on = dt_from_string->subtract( days => 8 )->date;
4522 my $item = $builder->build_sample_item(
4524 biblionumber => $biblionumber,
4525 library => $library->branchcode,
4526 replacementprice => '42',
4529 my $issue = AddIssue( $patron->unblessed, $item->barcode );
4530 LostItem( $item->itemnumber, 'cli', 0 );
4531 $item->_result->itemlost(1);
4532 $item->_result->itemlost_on( $lost_on );
4533 $item->_result->update();
4535 my $a = Koha::Account::Lines->search(
4537 itemnumber => $item->id,
4538 borrowernumber => $patron->borrowernumber
4541 $a = $a->next;
4542 ok( $a, "Found accountline for lost fee" );
4543 is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
4544 $issue = AddIssue( $patron2->unblessed, $item->barcode );
4545 $a = $a->get_from_storage;
4546 is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
4547 $a->delete;
4551 subtest 'transferbook tests' => sub {
4552 plan tests => 9;
4554 throws_ok
4555 { C4::Circulation::transferbook({}); }
4556 'Koha::Exceptions::MissingParameter',
4557 'Koha::Patron->store raises an exception on missing params';
4559 throws_ok
4560 { C4::Circulation::transferbook({to_branch=>'anything'}); }
4561 'Koha::Exceptions::MissingParameter',
4562 'Koha::Patron->store raises an exception on missing params';
4564 throws_ok
4565 { C4::Circulation::transferbook({from_branch=>'anything'}); }
4566 'Koha::Exceptions::MissingParameter',
4567 'Koha::Patron->store raises an exception on missing params';
4569 my ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here'});
4570 is( $doreturn, 0, "No return without barcode");
4571 ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
4572 is( $messages->{BadBarcode}, undef, "No barcode passed means undef BadBarcode" );
4574 ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here',barcode=>'BadBarcode'});
4575 is( $doreturn, 0, "No return without barcode");
4576 ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
4577 is( $messages->{BadBarcode}, 'BadBarcode', "No barcode passed means undef BadBarcode" );
4581 subtest 'Checkout should correctly terminate a transfer' => sub {
4582 plan tests => 7;
4584 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
4585 my $patron_1 = $builder->build_object(
4587 class => 'Koha::Patrons',
4588 value => { branchcode => $library_1->branchcode }
4591 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
4592 my $patron_2 = $builder->build_object(
4594 class => 'Koha::Patrons',
4595 value => { branchcode => $library_2->branchcode }
4599 my $item = $builder->build_sample_item(
4601 library => $library_1->branchcode,
4605 t::lib::Mocks::mock_userenv( { branchcode => $library_1->branchcode } );
4606 my $reserve_id = AddReserve(
4608 branchcode => $library_2->branchcode,
4609 borrowernumber => $patron_2->borrowernumber,
4610 biblionumber => $item->biblionumber,
4611 itemnumber => $item->itemnumber,
4612 priority => 1,
4616 my $do_transfer = 1;
4617 ModItemTransfer( $item->itemnumber, $library_1->branchcode,
4618 $library_2->branchcode );
4619 ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4620 GetOtherReserves( $item->itemnumber )
4621 ; # To put the Reason, it's what does returns.pl...
4622 my $hold = Koha::Holds->find($reserve_id);
4623 is( $hold->found, 'T', 'Hold is in transit' );
4624 my $transfer = $item->get_transfer;
4625 is( $transfer->frombranch, $library_1->branchcode );
4626 is( $transfer->tobranch, $library_2->branchcode );
4627 is( $transfer->reason, 'Reserve' );
4629 t::lib::Mocks::mock_userenv( { branchcode => $library_2->branchcode } );
4630 AddIssue( $patron_1->unblessed, $item->barcode );
4631 $transfer = $transfer->get_from_storage;
4632 isnt( $transfer->datearrived, undef );
4633 $hold = $hold->get_from_storage;
4634 is( $hold->found, undef, 'Hold is waiting' );
4635 is( $hold->priority, 1, );
4638 subtest 'AddIssue records staff who checked out item if appropriate' => sub {
4639 plan tests => 2;
4641 $module->mock( 'userenv', sub { { branch => $library->{id} } } );
4643 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
4644 my $patron = $builder->build_object(
4646 class => 'Koha::Patrons',
4647 value => { categorycode => $patron_category->{categorycode} }
4650 my $issuer = $builder->build_object(
4652 class => 'Koha::Patrons',
4653 value => { categorycode => $patron_category->{categorycode} }
4656 my $item = $builder->build_sample_item(
4658 library => $library->{branchcode}
4662 $module->mock( 'userenv', sub { { branch => $library->id, number => $issuer->{borrowernumber} } } );
4664 my $dt_from = dt_from_string();
4665 my $dt_to = dt_from_string()->add( days => 7 );
4667 my $issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4669 is( $issue->issuer, undef, "Staff who checked out the item not recorded when RecordStaffUserOnCheckout turned off" );
4671 t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 1);
4673 my $issue2 =
4674 AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
4676 is( $issue->issuer, $issuer->{borrowernumber}, "Staff who checked out the item recorded when RecordStaffUserOnCheckout turned on" );
4679 $schema->storage->txn_rollback;
4680 C4::Context->clear_syspref_cache();
4681 $branches = Koha::Libraries->search();
4682 for my $branch ( $branches->next ) {
4683 my $key = $branch->branchcode . "_holidays";
4684 $cache->clear_from_cache($key);