3 # Copyright 2015 Koha Development team
5 # This file is part of Koha
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Test::More tests => 12;
26 use C4::Circulation qw( MarkIssueReturned AddReturn );
27 use C4::Reserves qw( AddReserve );
30 use Koha::DateUtils qw( dt_from_string );
33 use t::lib::TestBuilder;
36 my $schema = Koha::Database->new->schema;
37 $schema->storage->txn_begin;
39 my $builder = t::lib::TestBuilder->new;
40 my $library = $builder->build( { source => 'Branch' } );
41 my $patron = $builder->build(
42 { source => 'Borrower', value => { branchcode => $library->{branchcode} } }
44 my $item_1 = $builder->build_sample_item;
45 my $item_2 = $builder->build_sample_item;
46 my $nb_of_checkouts = Koha::Checkouts->search->count;
47 my $new_checkout_1 = Koha::Checkout->new(
49 borrowernumber => $patron->{borrowernumber},
50 itemnumber => $item_1->itemnumber,
51 branchcode => $library->{branchcode},
54 my $new_checkout_2 = Koha::Checkout->new(
56 borrowernumber => $patron->{borrowernumber},
57 itemnumber => $item_2->itemnumber,
58 branchcode => $library->{branchcode},
62 like( $new_checkout_1->issue_id, qr|^\d+$|,
63 'Adding a new checkout should have set the issue_id' );
65 Koha::Checkouts->search->count,
67 'The 2 checkouts should have been added'
70 my $retrieved_checkout_1 = Koha::Checkouts->find( $new_checkout_1->issue_id );
72 $retrieved_checkout_1->itemnumber,
73 $new_checkout_1->itemnumber,
74 'Find a checkout by id should return the correct checkout'
77 subtest 'is_overdue' => sub {
79 my $ten_days_ago = dt_from_string->add( days => -10 );
80 my $ten_days_later = dt_from_string->add( days => 10 );
81 my $yesterday = dt_from_string->add( days => -1 );
82 my $tomorrow = dt_from_string->add( days => 1 );
84 $retrieved_checkout_1->date_due($ten_days_ago)->store;
85 is( $retrieved_checkout_1->is_overdue,
86 1, 'The item should have been returned 10 days ago' );
88 $retrieved_checkout_1->date_due($ten_days_later)->store;
89 is( $retrieved_checkout_1->is_overdue, 0, 'The item is due in 10 days' );
91 $retrieved_checkout_1->date_due($tomorrow)->store;
92 is( $retrieved_checkout_1->is_overdue($ten_days_later),
93 1, 'The item should have been returned yesterday' );
95 $retrieved_checkout_1->date_due($yesterday)->store;
96 is( $retrieved_checkout_1->is_overdue($ten_days_ago),
97 0, 'Ten days ago the item due yesterday was not late' );
99 $retrieved_checkout_1->date_due($tomorrow)->store;
100 is( $retrieved_checkout_1->is_overdue($ten_days_later),
101 1, 'In Ten days, the item due tomorrow will be late' );
103 $retrieved_checkout_1->date_due($yesterday)->store;
104 is( $retrieved_checkout_1->is_overdue($ten_days_ago),
105 0, 'In Ten days, the item due yesterday will still be late' );
108 subtest 'item' => sub {
110 my $item = $retrieved_checkout_1->item;
111 is( ref($item), 'Koha::Item',
112 'Koha::Checkout->item should return a Koha::Item' );
113 is( $item->itemnumber, $item_1->itemnumber,
114 'Koha::Checkout->item should return the correct item' );
117 subtest 'account_lines' => sub {
120 my $accountline = Koha::Account::Line->new(
122 issue_id => $retrieved_checkout_1->id,
123 borrowernumber => $retrieved_checkout_1->borrowernumber,
124 itemnumber => $retrieved_checkout_1->itemnumber,
125 branchcode => $retrieved_checkout_1->branchcode,
127 debit_type_code => 'OVERDUE',
128 status => 'UNRETURNED',
131 amountoutstanding => '1',
135 my $account_lines = $retrieved_checkout_1->account_lines;
136 is( ref($account_lines), 'Koha::Account::Lines',
137 'Koha::Checkout->account_lines should return a Koha::Account::Lines' );
139 my $line = $account_lines->next;
140 is( ref($line), 'Koha::Account::Line',
141 'next returns a Koha::Account::Line' );
146 'Koha::Checkout->account_lines should return the correct account_lines'
150 subtest 'patron' => sub {
152 my $patron = $builder->build_object(
154 class => 'Koha::Patrons',
155 value => { branchcode => $library->{branchcode} }
159 my $item = $builder->build_sample_item;
160 my $checkout = Koha::Checkout->new(
162 borrowernumber => $patron->borrowernumber,
163 itemnumber => $item->itemnumber,
164 branchcode => $library->{branchcode},
168 my $p = $checkout->patron;
169 is( ref($p), 'Koha::Patron',
170 'Koha::Checkout->patron should return a Koha::Patron' );
171 is( $p->borrowernumber, $patron->borrowernumber,
172 'Koha::Checkout->patron should return the correct patron' );
174 # Testing Koha::Old::Checkout->patron now
175 my $issue_id = $checkout->issue_id;
176 C4::Circulation::MarkIssueReturned( $p->borrowernumber,
177 $checkout->itemnumber );
179 my $old_issue = Koha::Old::Checkouts->find($issue_id);
180 is( $old_issue->patron, undef,
181 'Koha::Checkout->patron should return undef if the patron record has been deleted'
185 $retrieved_checkout_1->delete;
187 Koha::Checkouts->search->count,
188 $nb_of_checkouts + 1,
189 'Delete should have deleted the checkout'
192 subtest 'issuer' => sub {
194 my $patron = $builder->build_object(
196 class => 'Koha::Patrons',
197 value => { branchcode => $library->{branchcode} }
200 my $issuer = $builder->build_object(
202 class => 'Koha::Patrons',
203 value => { branchcode => $library->{branchcode} }
207 my $item = $builder->build_sample_item;
208 my $checkout = Koha::Checkout->new(
210 borrowernumber => $patron->borrowernumber,
211 issuer_id => $issuer->borrowernumber,
212 itemnumber => $item->itemnumber,
213 branchcode => $library->{branchcode},
217 my $i = $checkout->issuer;
218 is( ref($i), 'Koha::Patron',
219 'Koha::Checkout->issuer should return a Koha::Patron' );
220 is( $i->borrowernumber, $issuer->borrowernumber,
221 'Koha::Checkout->issuer should return the correct patron' );
223 # Testing Koha::Old::Checkout->patron now
224 my $issue_id = $checkout->issue_id;
225 C4::Circulation::MarkIssueReturned( $patron->borrowernumber,
226 $checkout->itemnumber );
228 my $old_issue = Koha::Old::Checkouts->find($issue_id);
229 is( $old_issue->issuer_id, undef,
230 'Koha::Checkout->issuer_id should return undef if the patron record has been deleted'
235 subtest 'Koha::Old::Checkouts->filter_by_todays_checkins' => sub {
239 # We will create 7 checkins for a given patron
240 # 3 checked in today - 2 days, and 4 checked in today
241 my $librarian = $builder->build_object(
243 class => 'Koha::Patrons',
244 value => { branchcode => $library->{branchcode} }
247 t::lib::Mocks::mock_userenv( { patron => $librarian } );
248 my $patron = $builder->build_object(
250 class => 'Koha::Patrons',
251 value => { branchcode => $library->{branchcode} }
258 my $item = $builder->build_sample_item;
262 borrowernumber => $patron->borrowernumber,
263 itemnumber => $item->itemnumber,
264 branchcode => $library->{branchcode},
269 # Checkin 3 today - 2 days
270 my $not_today = dt_from_string->add( days => -2 );
271 for my $i ( 0 .. 2 ) {
272 my $checkout = $checkouts[$i];
273 C4::Circulation::AddReturn(
274 $checkout->item->barcode, $library->{branchcode},
275 undef, $not_today->set_hour( int( rand(24) ) )
279 my $today = dt_from_string;
280 for my $i ( 3 .. 6 ) {
281 my $checkout = $checkouts[$i];
282 C4::Circulation::AddReturn(
283 $checkout->item->barcode, $library->{branchcode},
284 undef, $today->set_hour( int( rand(24) ) )
288 my $old_checkouts = $patron->old_checkouts;
289 is( $old_checkouts->count, 7, 'There should be 7 old checkouts' );
290 my $todays_checkins = $old_checkouts->filter_by_todays_checkins;
291 is( $todays_checkins->count, 4, 'There should be 4 checkins today' );
293 [ $todays_checkins->get_column('itemnumber') ],
294 [ map { $_->itemnumber } @checkouts[ 3 .. 6 ] ],
295 q{Correct list of today's checkins}
299 $schema->storage->txn_rollback;
301 subtest 'automatic_checkin' => sub {
305 $schema->storage->txn_begin;
307 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
310 $builder->build_sample_item(
311 { homebranch => $patron->branchcode, itemlost => 0 } );
313 $builder->build_sample_item(
314 { homebranch => $patron->branchcode, itemlost => 0 } );
316 $builder->build_sample_item(
317 { homebranch => $patron->branchcode, itemlost => 0 } );
319 $builder->build_sample_item(
320 { homebranch => $patron->branchcode, itemlost => 0 } );
322 $due_ac_item->itemtype->automatic_checkin(1)->store;
323 $odue_ac_item->itemtype->automatic_checkin(1)->store;
324 $ac_item->itemtype->automatic_checkin(1)->store;
325 $normal_item->itemtype->automatic_checkin(0)->store;
327 my $today = dt_from_string;
328 my $tomorrow = dt_from_string->add( days => 1 );
329 my $yesterday = dt_from_string->subtract( days => 1 );
331 # Checkout do for automatic checkin
332 my $checkout_due_aci = Koha::Checkout->new(
334 borrowernumber => $patron->borrowernumber,
335 itemnumber => $due_ac_item->itemnumber,
336 branchcode => $patron->branchcode,
341 # Checkout not due for automatic checkin
342 my $checkout_odue_aci = Koha::Checkout->new(
344 borrowernumber => $patron->borrowernumber,
345 itemnumber => $odue_ac_item->itemnumber,
346 branchcode => $patron->branchcode,
347 date_due => $yesterday
351 # Checkout not due for automatic checkin
352 my $checkout_aci = Koha::Checkout->new(
354 borrowernumber => $patron->borrowernumber,
355 itemnumber => $ac_item->itemnumber,
356 branchcode => $patron->branchcode,
357 date_due => $tomorrow
361 # due checkout for nomal itemtype
362 my $checkout_ni = Koha::Checkout->new(
364 borrowernumber => $patron->borrowernumber,
365 itemnumber => $normal_item->itemnumber,
366 branchcode => $patron->branchcode,
371 my $searched = Koha::Checkouts->find( $checkout_ni->issue_id );
372 is( $searched->issue_id, $checkout_ni->issue_id,
373 'checkout for normal_item exists' );
375 $searched = Koha::Checkouts->find( $checkout_aci->issue_id );
376 is( $searched->issue_id, $checkout_aci->issue_id,
377 'checkout for ac_item exists' );
379 $searched = Koha::Checkouts->find( $checkout_due_aci->issue_id );
382 $checkout_due_aci->issue_id,
383 'checkout for due_ac_item exists'
386 $searched = Koha::Checkouts->find( $checkout_odue_aci->issue_id );
389 $checkout_odue_aci->issue_id,
390 'checkout for odue_ac_item exists'
393 Koha::Checkouts->automatic_checkin;
395 $searched = Koha::Checkouts->find( $checkout_ni->issue_id );
396 is( $searched->issue_id, $checkout_ni->issue_id,
397 'checkout for normal_item still exists' );
399 $searched = Koha::Checkouts->find( $checkout_aci->issue_id );
400 is( $searched->issue_id, $checkout_aci->issue_id,
401 'checkout for ac_item still exists' );
403 $searched = Koha::Checkouts->find( $checkout_due_aci->issue_id );
404 is( $searched, undef, 'checkout for due_ac_item doesn\'t exist anymore' );
406 $searched = Koha::Checkouts->find( $checkout_odue_aci->issue_id );
407 is( $searched, undef, 'checkout for odue_ac_item doesn\'t exist anymore' );
409 $searched = Koha::Old::Checkouts->find( $checkout_odue_aci->issue_id );
410 is( dt_from_string($searched->returndate), $yesterday, 'old checkout for odue_ac_item has the right return date' );
413 subtest 'automatic_checkin AutomaticCheckinAutoFill tests' => sub {
417 my $checkout_2_due_ac = Koha::Checkout->new(
419 borrowernumber => $patron->borrowernumber,
420 itemnumber => $due_ac_item->itemnumber,
421 branchcode => $patron->branchcode,
427 $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $patron->branchcode } } );
428 my $reserveid = AddReserve(
430 branchcode => $patron->branchcode,
431 borrowernumber => $patron_2->id,
432 biblionumber => $due_ac_item->biblionumber,
437 t::lib::Mocks::mock_preference( 'AutomaticCheckinAutoFill', '0' );
439 Koha::Checkouts->automatic_checkin;
440 my $reserve = Koha::Holds->find($reserveid);
442 is( $reserve->found, undef, "Hold was not filled when AutomaticCheckinAutoFill disabled" );
444 my $checkout_3_due_ac = Koha::Checkout->new(
446 borrowernumber => $patron->borrowernumber,
447 itemnumber => $due_ac_item->itemnumber,
448 branchcode => $patron->branchcode,
452 t::lib::Mocks::mock_preference( 'AutomaticCheckinAutoFill', '1' );
454 Koha::Checkouts->automatic_checkin;
455 $reserve->discard_changes;
457 is( $reserve->found, 'W', "Hold was filled when AutomaticCheckinAutoFill enabled" );
459 my $checkout_2_odue_ac = Koha::Checkout->new(
461 borrowernumber => $patron->borrowernumber,
462 itemnumber => $odue_ac_item->itemnumber,
463 branchcode => $patron->branchcode,
467 my $branch2 = $builder->build_object( { class => "Koha::Libraries" } );
468 my $reserve2id = AddReserve(
470 branchcode => $branch2->branchcode,
471 borrowernumber => $patron_2->id,
472 biblionumber => $odue_ac_item->biblionumber,
476 Koha::Checkouts->automatic_checkin;
478 my $reserve2 = Koha::Holds->find($reserve2id);
480 $reserve2->found, 'T',
481 "Hold was filled when AutomaticCheckinAutoFill enabled and transfer was initiated when branches didn't match"
485 $schema->storage->txn_rollback;
488 subtest 'attempt_auto_renew' => sub {
492 $schema->storage->txn_begin;
494 my $renew_error = 'auto_renew';
495 my $module = Test::MockModule->new('C4::Circulation');
496 $module->mock( 'CanBookBeRenewed', sub { return ( 1, $renew_error ) } );
497 $module->mock( 'AddRenewal', sub { warn "AddRenewal called" } );
498 my $checkout = $builder->build_object(
500 class => 'Koha::Checkouts',
502 date_due => '2023-01-01 23:59:59',
505 auto_renew_error => undef,
506 onsite_checkout => 0,
512 my ( $success, $error, $updated );
514 ( $success, $error, $updated ) = $checkout->attempt_auto_renew();
516 undef, "AddRenewal not called without confirm";
517 ok( $success, "Issue is renewed when error is 'auto_renew'" );
518 is( $error, undef, "No error when renewed" );
519 ok( $updated, "Issue reported as updated when renewed" );
522 ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
524 "AddRenewal called", "AddRenewal called when confirm is passed";
525 ok( $success, "Issue is renewed when error is 'auto_renew'" );
526 is( $error, undef, "No error when renewed" );
527 ok( $updated, "Issue reported as updated when renewed" );
529 $module->mock( 'AddRenewal', sub { return; } );
531 $renew_error = 'anything_else';
532 ( $success, $error, $updated ) = $checkout->attempt_auto_renew();
533 ok( !$success, "Success is untrue for any other status" );
534 is( $error, 'anything_else', "The error is passed through" );
535 ok( $updated, "Issue reported as updated when status changes" );
536 $checkout->discard_changes();
537 is( $checkout->auto_renew_error, undef, "Error not updated if confirm not passed" );
539 ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
540 ok( !$success, "Success is untrue for any other status" );
541 is( $error, 'anything_else', "The error is passed through" );
542 ok( $updated, "Issue updated when confirm passed" );
543 $checkout->discard_changes();
544 is( $checkout->auto_renew_error, 'anything_else', "Error updated if confirm passed" );
546 # Error now equals 'anything_else'
547 ( $success, $error, $updated ) = $checkout->attempt_auto_renew();
548 ok( !$updated, "Issue not reported as updated when status has not changed" );
550 $renew_error = "auto_unseen_final";
551 ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
552 ok( $success, "Issue is renewed when error is 'auto_unseen_final'" );
553 is( $error, 'auto_unseen_final', "Error of finality reported when renewed" );
554 ok( $updated, "Issue reported as updated when renewed" );
555 $checkout->discard_changes();
556 is( $checkout->auto_renew_error, 'auto_unseen_final', "Error updated" );
558 $renew_error = "too_unseen";
559 ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
560 ok( !$success, "Issue is not renewed when error is 'too_unseen'" );
561 is( $error, 'too_unseen', "Error reported correctly" );
562 ok( !$updated, "Issue not reported as updated when moved from final to too unseen" );
563 $checkout->discard_changes();
564 is( $checkout->auto_renew_error, 'too_unseen', "Error updated" );
566 $renew_error = "auto_renew_final";
567 ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
568 ok( $success, "Issue is renewed when error is 'auto_renew_final'" );
569 is( $error, 'auto_renew_final', "Error of finality reported when renewed" );
570 ok( $updated, "Issue reported as updated when renewed" );
571 $checkout->discard_changes();
572 is( $checkout->auto_renew_error, 'auto_renew_final', "Error updated" );
574 $renew_error = "too_many";
575 ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
576 ok( !$success, "Issue is not renewed when error is 'too_many'" );
577 is( $error, 'too_many', "Error reported correctly" );
578 ok( !$updated, "Issue not reported as updated when moved from final to too many" );
579 $checkout->discard_changes();
580 is( $checkout->auto_renew_error, 'too_many', "Error updated" );
582 $schema->storage->txn_rollback;