Bug 36567: Get rid of Datetime warning in Circulation.t and dateexpiry.t
[koha.git] / t / db_dependent / Circulation.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
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.
9 #
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.
14 #
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>.
17
18 use Modern::Perl;
19 use utf8;
20
21 use Test::More tests => 70;
22 use Test::Exception;
23 use Test::MockModule;
24 use Test::Deep qw( cmp_deeply );
25 use Test::Warn;
26
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;
33
34 use C4::Accounts;
35 use C4::Calendar qw( new insert_single_holiday insert_week_day_holiday delete_holiday );
36 use C4::Circulation qw( AddIssue AddReturn CanBookBeRenewed GetIssuingCharges AddRenewal GetSoonestRenewDate GetLatestAutoRenewDate LostItem GetUpcomingDueIssues CanBookBeIssued AddIssuingCharge MarkIssueReturned ProcessOfflinePayment transferbook updateWrongTransfer );
37 use C4::Biblio;
38 use C4::Items qw( ModItemTransfer );
39 use C4::Log;
40 use C4::Reserves qw( AddReserve ModReserve ModReserveCancelAll ModReserveAffect CheckReserves GetOtherReserves );
41 use C4::Overdues qw( CalcFine UpdateFine get_chargeable_units );
42 use C4::Members::Messaging qw( SetMessagingPreference );
43 use Koha::DateUtils qw( dt_from_string output_pref );
44 use Koha::Database;
45 use Koha::Bookings;
46 use Koha::Items;
47 use Koha::Item::Transfers;
48 use Koha::Checkouts;
49 use Koha::Patrons;
50 use Koha::Patron::Debarments qw( AddDebarment DelUniqueDebarment );
51 use Koha::Holds;
52 use Koha::CirculationRules;
53 use Koha::Subscriptions;
54 use Koha::Account::Lines;
55 use Koha::Account::Offsets;
56 use Koha::ActionLogs;
57 use Koha::Notice::Messages;
58 use Koha::Cache::Memory::Lite;
59
60 my $builder = t::lib::TestBuilder->new;
61 sub set_userenv {
62     my ( $library ) = @_;
63     my $staff = $builder->build_object({ class => "Koha::Patrons" });
64     t::lib::Mocks::mock_userenv({ patron => $staff, branchcode => $library->{branchcode} });
65 }
66
67 sub str {
68     my ( $error, $question, $alert ) = @_;
69     my $s;
70     $s  = %$error    ? ' (error: '    . join( ' ', keys %$error    ) . ')' : '';
71     $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
72     $s .= %$alert    ? ' (alert: '    . join( ' ', keys %$alert    ) . ')' : '';
73     return $s;
74 }
75
76 sub test_debarment_on_checkout {
77     my ($params) = @_;
78     my $item     = $params->{item};
79     my $library  = $params->{library};
80     my $patron   = $params->{patron};
81     my $due_date = $params->{due_date} || dt_from_string;
82     my $return_date = $params->{return_date} || dt_from_string;
83     my $expected_expiration_date = $params->{expiration_date};
84
85     $expected_expiration_date = output_pref(
86         {
87             dt         => $expected_expiration_date,
88             dateformat => 'sql',
89             dateonly   => 1,
90         }
91     );
92     my @caller      = caller;
93     my $line_number = $caller[2];
94     AddIssue( $patron, $item->barcode, $due_date );
95
96     my ( undef, $message ) = AddReturn( $item->barcode, $library->{branchcode}, undef, $return_date );
97     is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
98         or diag('AddReturn returned message ' . Dumper $message );
99     my $suspensions = $patron->restrictions->search({ type => 'SUSPENSION' } );
100     is( $suspensions->count, 1, 'Test at line ' . $line_number );
101
102     my $THE_suspension = $suspensions->next;
103     is( $THE_suspension->expiration,
104         $expected_expiration_date, 'Test at line ' . $line_number );
105     Koha::Patron::Debarments::DelUniqueDebarment(
106         { borrowernumber => $patron->borrowernumber, type => 'SUSPENSION' } );
107 };
108
109 my $schema = Koha::Database->schema;
110 $schema->storage->txn_begin;
111 my $dbh = C4::Context->dbh;
112
113 # Prevent random failures by mocking ->now
114 my $now_value       = dt_from_string;
115 my $mocked_datetime = Test::MockModule->new('DateTime');
116 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
117
118 my $cache = Koha::Caches->get_instance();
119 $dbh->do(q|DELETE FROM special_holidays|);
120 $dbh->do(q|DELETE FROM repeatable_holidays|);
121 my $branches = Koha::Libraries->search();
122 for my $branch ( $branches->next ) {
123     my $key = $branch->branchcode . "_holidays";
124     $cache->clear_from_cache($key);
125 }
126
127 # Start with a clean slate
128 $dbh->do('DELETE FROM issues');
129 $dbh->do('DELETE FROM borrowers');
130
131 # Disable recording of the staff who checked out an item until we're ready for it
132 t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 0);
133
134 my $module = Test::MockModule->new('C4::Context');
135
136 my $library = $builder->build({
137     source => 'Branch',
138 });
139 my $library2 = $builder->build({
140     source => 'Branch',
141 });
142 my $itemtype = $builder->build(
143     {
144         source => 'Itemtype',
145         value  => {
146             notforloan          => undef,
147             rentalcharge        => 0,
148             rentalcharge_daily => 0,
149             defaultreplacecost  => undef,
150             processfee          => undef
151         }
152     }
153 )->{itemtype};
154 my $patron_category = $builder->build(
155     {
156         source => 'Category',
157         value  => {
158             category_type                 => 'P',
159             enrolmentfee                  => 0,
160             BlockExpiredPatronOpacActions => -1, # Pick the pref value
161         }
162     }
163 );
164
165 my $CircControl = C4::Context->preference('CircControl');
166 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
167
168 my $item = $builder->build_object({
169     class => 'Koha::Items',
170     value => {
171         homebranch => $library2->{branchcode},
172         holdingbranch => $library2->{branchcode}
173     }
174 });
175
176 my $borrower = $builder->build_object({
177     class => 'Koha::Patrons',
178     value => { branchcode => $library2->{branchcode} }
179 });
180
181 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
182
183 # No userenv, PickupLibrary
184 t::lib::Mocks::mock_preference('IndependentBranches', '0');
185 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
186 is(
187     C4::Context->preference('CircControl'),
188     'PickupLibrary',
189     'CircControl changed to PickupLibrary'
190 );
191 is(
192     C4::Circulation::_GetCircControlBranch($item, $borrower),
193     $item->get_column($HomeOrHoldingBranch),
194     '_GetCircControlBranch returned item branch (no userenv defined)'
195 );
196
197 # No userenv, PatronLibrary
198 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
199 is(
200     C4::Context->preference('CircControl'),
201     'PatronLibrary',
202     'CircControl changed to PatronLibrary'
203 );
204 is(
205     C4::Circulation::_GetCircControlBranch($item, $borrower),
206     $borrower->branchcode,
207     '_GetCircControlBranch returned borrower branch'
208 );
209
210 # No userenv, ItemHomeLibrary
211 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
212 is(
213     C4::Context->preference('CircControl'),
214     'ItemHomeLibrary',
215     'CircControl changed to ItemHomeLibrary'
216 );
217 is(
218     $item->get_column($HomeOrHoldingBranch),
219     C4::Circulation::_GetCircControlBranch($item, $borrower),
220     '_GetCircControlBranch returned item branch'
221 );
222
223 # Now, set a userenv
224 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
225 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
226
227 # Userenv set, PickupLibrary
228 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
229 is(
230     C4::Context->preference('CircControl'),
231     'PickupLibrary',
232     'CircControl changed to PickupLibrary'
233 );
234 is(
235     C4::Circulation::_GetCircControlBranch($item, $borrower),
236     $library2->{branchcode},
237     '_GetCircControlBranch returned current branch'
238 );
239
240 # Userenv set, PatronLibrary
241 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
242 is(
243     C4::Context->preference('CircControl'),
244     'PatronLibrary',
245     'CircControl changed to PatronLibrary'
246 );
247 is(
248     C4::Circulation::_GetCircControlBranch($item, $borrower),
249     $borrower->branchcode,
250     '_GetCircControlBranch returned borrower branch'
251 );
252
253 # Userenv set, ItemHomeLibrary
254 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
255 is(
256     C4::Context->preference('CircControl'),
257     'ItemHomeLibrary',
258     'CircControl changed to ItemHomeLibrary'
259 );
260 is(
261     C4::Circulation::_GetCircControlBranch($item, $borrower),
262     $item->get_column($HomeOrHoldingBranch),
263     '_GetCircControlBranch returned item branch'
264 );
265
266 # Reset initial configuration
267 t::lib::Mocks::mock_preference('CircControl', $CircControl);
268 is(
269     C4::Context->preference('CircControl'),
270     $CircControl,
271     'CircControl reset to its initial value'
272 );
273
274 # Set a simple circ policy
275 $dbh->do('DELETE FROM circulation_rules');
276 Koha::CirculationRules->set_rules(
277     {
278         categorycode => undef,
279         branchcode   => undef,
280         itemtype     => undef,
281         rules        => {
282             reservesallowed     => 25,
283             issuelength         => 14,
284             lengthunit          => 'days',
285             renewalsallowed     => 1,
286             renewalperiod       => 7,
287             norenewalbefore     => undef,
288             noautorenewalbefore => undef,
289             auto_renew          => 0,
290             fine                => .10,
291             chargeperiod        => 1,
292         }
293     }
294 );
295
296 subtest "CanBookBeRenewed AllowRenewalIfOtherItemsAvailable multiple borrowers and items tests" => sub {
297     plan tests => 7;
298
299     #Can only reserve from home branch
300     Koha::CirculationRules->set_rule(
301         {
302             branchcode   => undef,
303             itemtype     => undef,
304             rule_name    => 'holdallowed',
305             rule_value   => 1
306         }
307     );
308     Koha::CirculationRules->set_rule(
309         {
310             branchcode   => undef,
311             categorycode   => undef,
312             itemtype     => undef,
313             rule_name    => 'onshelfholds',
314             rule_value   => 1
315         }
316     );
317
318     # Patrons from three different branches
319     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
320     my $patron_hold_1   = $builder->build_object({ class => 'Koha::Patrons' });
321     my $patron_hold_2   = $builder->build_object({ class => 'Koha::Patrons' });
322     my $biblio = $builder->build_sample_biblio();
323
324     # Item at each patron branch
325     my $item_1 = $builder->build_sample_item({
326         biblionumber => $biblio->biblionumber,
327         homebranch   => $patron->branchcode
328     });
329     my $item_2 = $builder->build_sample_item({
330         biblionumber => $biblio->biblionumber,
331         homebranch   => $patron_hold_2->branchcode
332     });
333     my $item_3 = $builder->build_sample_item({
334         biblionumber => $biblio->biblionumber,
335         homebranch   => $patron_hold_1->branchcode
336     });
337
338     my $issue = AddIssue( $patron, $item_1->barcode);
339     my $datedue = dt_from_string( $issue->date_due() );
340     is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
341
342     # Biblio-level holds
343     my $reserve_1 = AddReserve(
344         {
345             branchcode       => $patron_hold_1->branchcode,
346             borrowernumber   => $patron_hold_1->borrowernumber,
347             biblionumber     => $biblio->biblionumber,
348             priority         => 1,
349             reservation_date => dt_from_string(),
350             expiration_date  => undef,
351             itemnumber       => undef,
352             found            => undef,
353         }
354     );
355     AddReserve(
356         {
357             branchcode       => $patron_hold_2->branchcode,
358             borrowernumber   => $patron_hold_2->borrowernumber,
359             biblionumber     => $biblio->biblionumber,
360             priority         => 2,
361             reservation_date => dt_from_string(),
362             expiration_date  => undef,
363             itemnumber       => undef,
364             found            => undef,
365         }
366     );
367     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
368
369     my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue);
370     is( $renewokay, 0, 'Cannot renew, reserved');
371     is( $error, 'on_reserve', 'Cannot renew, reserved (returned error is on_reserve)');
372
373     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
374
375     ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue);
376     is( $renewokay, 1, 'Can renew, two items available for two holds');
377     is( $error, undef, 'Can renew, each reserve has an item');
378
379     # Item level hold
380     my $hold = Koha::Holds->find( $reserve_1 );
381     $hold->itemnumber( $item_1->itemnumber )->store;
382
383     ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue);
384     is( $renewokay, 0, 'Cannot renew when there is an item specific hold');
385     is( $error, 'on_reserve', 'Cannot renew, only this item can fill the reserve');
386 };
387
388 subtest "GetIssuingCharges tests" => sub {
389     plan tests => 4;
390     my $branch_discount = $builder->build_object({ class => 'Koha::Libraries' });
391     my $branch_no_discount = $builder->build_object({ class => 'Koha::Libraries' });
392     Koha::CirculationRules->set_rule(
393         {
394             categorycode => undef,
395             branchcode   => $branch_discount->branchcode,
396             itemtype     => undef,
397             rule_name    => 'rentaldiscount',
398             rule_value   => 15
399         }
400     );
401     my $itype_charge = $builder->build_object({
402         class => 'Koha::ItemTypes',
403         value => {
404             rentalcharge => 10
405         }
406     });
407     my $itype_no_charge = $builder->build_object({
408         class => 'Koha::ItemTypes',
409         value => {
410             rentalcharge => 0
411         }
412     });
413     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
414     my $item_1 = $builder->build_sample_item({ itype => $itype_charge->itemtype });
415     my $item_2 = $builder->build_sample_item({ itype => $itype_no_charge->itemtype });
416
417     t::lib::Mocks::mock_userenv({ branchcode => $branch_no_discount->branchcode });
418     # For now the sub always uses the env branch, this should follow CircControl instead
419     my ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
420     is( $charge + 0, 10.00, "Charge fetched correctly when no discount exists");
421     ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
422     is( $charge + 0, 0.00, "Charge fetched correctly when no discount exists and no charge");
423
424     t::lib::Mocks::mock_userenv({ branchcode => $branch_discount->branchcode });
425     # For now the sub always uses the env branch, this should follow CircControl instead
426     ($charge, $itemtype) = GetIssuingCharges( $item_1->itemnumber, $patron->borrowernumber);
427     is( $charge + 0, 8.50, "Charge fetched correctly when discount exists");
428     ($charge, $itemtype) = GetIssuingCharges( $item_2->itemnumber, $patron->borrowernumber);
429     is( $charge + 0, 0.00, "Charge fetched correctly when discount exists and no charge");
430
431 };
432
433 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
434 subtest "CanBookBeRenewed tests" => sub {
435     plan tests => 114;
436
437     C4::Context->set_preference('ItemsDeniedRenewal','');
438     # Generate test biblio
439     my $biblio = $builder->build_sample_biblio();
440
441     my $branch = $library2->{branchcode};
442
443     my $item_1 = $builder->build_sample_item(
444         {
445             biblionumber     => $biblio->biblionumber,
446             library          => $branch,
447             replacementprice => 12.00,
448             itype            => $itemtype
449         }
450     );
451     $reused_itemnumber_1 = $item_1->itemnumber;
452
453     my $item_2 = $builder->build_sample_item(
454         {
455             biblionumber     => $biblio->biblionumber,
456             library          => $branch,
457             replacementprice => 23.00,
458             itype            => $itemtype
459         }
460     );
461     $reused_itemnumber_2 = $item_2->itemnumber;
462
463     my $item_3 = $builder->build_sample_item(
464         {
465             biblionumber     => $biblio->biblionumber,
466             library          => $branch,
467             replacementprice => 23.00,
468             itype            => $itemtype
469         }
470     );
471
472     # Create borrowers
473
474     my $renewing_borrower_obj = $builder->build_object(
475         {
476             class => 'Koha::Patrons',
477             value => {
478                 firstname =>  'John',
479                 surname => 'Renewal',
480                 categorycode => $patron_category->{categorycode},
481                 branchcode => $branch,
482                 autorenew_checkouts => 1,
483             }
484         }
485     )->store;
486     my $renewing_borrowernumber = $renewing_borrower_obj->borrowernumber;
487
488     my $reserving_borrowernumber = $builder->build_object(
489         {
490             class => 'Koha::Patrons',
491             value => {
492                 firstname =>  'Katrin',
493                 surname => 'Reservation',
494                 categorycode => $patron_category->{categorycode},
495                 branchcode => $branch,
496             }
497         }
498     )->store->borrowernumber;
499
500     my $hold_waiting_borrowernumber = $builder->build_object(
501         {
502             class => 'Koha::Patrons',
503             value => {
504                 firstname =>  'Kyle',
505                 surname => 'Reservation',
506                 categorycode => $patron_category->{categorycode},
507                 branchcode => $branch,
508             }
509         }
510     )->store->borrowernumber;
511
512     my $restricted_borrower_obj = $builder->build_object(
513         {
514             class => 'Koha::Patrons',
515             value => {
516                 firstname =>  'Alice',
517                 surname => 'Reservation',
518                 categorycode => $patron_category->{categorycode},
519                 debarred => '3228-01-01',
520                 branchcode => $branch,
521             }
522         }
523     )->store;
524
525     my $expired_borrower_obj = $builder->build_object(
526         {
527             class => 'Koha::Patrons',
528             value => {
529                 firstname =>  'Ça',
530                 surname => 'Glisse',
531                 categorycode => $patron_category->{categorycode},
532                 branchcode => $branch,
533                 dateexpiry => dt_from_string->subtract( months => 1 ),
534                 autorenew_checkouts => 1,
535             }
536         }
537     );
538
539     my $bibitems       = '';
540     my $priority       = '1';
541     my $resdate        = undef;
542     my $expdate        = undef;
543     my $notes          = '';
544     my $checkitem      = undef;
545     my $found          = undef;
546
547     my $issue_1 = AddIssue( $renewing_borrower_obj, $item_1->barcode);
548     my $datedue = dt_from_string( $issue_1->date_due() );
549     is (defined $issue_1->date_due(), 1, "Item 1 checked out, due date: " . $issue_1->date_due() );
550
551     my $issue_2 = AddIssue( $renewing_borrower_obj, $item_2->barcode);
552     is (defined $issue_2, 1, "Item 2 checked out, due date: " . $issue_2->date_due());
553
554     my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
555     is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to ".$renewing_borrower_obj->firstname." ".$renewing_borrower_obj->surname);
556
557     my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1, 1);
558     is( $renewokay, 1, 'Can renew, no holds for this title or item');
559
560
561     # Biblio-level hold, renewal test
562     AddReserve(
563         {
564             branchcode       => $branch,
565             borrowernumber   => $reserving_borrowernumber,
566             biblionumber     => $biblio->biblionumber,
567             priority         => $priority,
568             reservation_date => $resdate,
569             expiration_date  => $expdate,
570             notes            => $notes,
571             itemnumber       => $checkitem,
572             found            => $found,
573         }
574     );
575
576     # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
577     Koha::CirculationRules->set_rule(
578         {
579             categorycode => undef,
580             branchcode   => undef,
581             itemtype     => undef,
582             rule_name    => 'onshelfholds',
583             rule_value   => '1',
584         }
585     );
586     Koha::CirculationRules->set_rule(
587         {
588             categorycode => undef,
589             branchcode   => undef,
590             itemtype     => undef,
591             rule_name    => 'renewalsallowed',
592             rule_value   => '5',
593         }
594     );
595     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
596     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
597     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
598     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
599     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
600
601
602     # Second biblio-level hold
603     my $reserve_id = AddReserve(
604         {
605             branchcode       => $branch,
606             borrowernumber   => $reserving_borrowernumber,
607             biblionumber     => $biblio->biblionumber,
608             priority         => $priority,
609             reservation_date => $resdate,
610             expiration_date  => $expdate,
611             notes            => $notes,
612             itemnumber       => $checkitem,
613             found            => $found,
614         }
615     );
616     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
617     is( $renewokay, 0, 'Renewal not possible when single patron\'s holds exceed the number of available items');
618     Koha::Holds->find($reserve_id)->delete;
619
620     # Now let's add an item level hold, we should no longer be able to renew the item
621     my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
622         {
623             borrowernumber => $hold_waiting_borrowernumber,
624             biblionumber   => $biblio->biblionumber,
625             itemnumber     => $item_1->itemnumber,
626             branchcode     => $branch,
627             priority       => 3,
628             reservedate    => '1999-01-01',
629         }
630     );
631     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
632     is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
633     $hold->delete();
634
635     # 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
636     # be able to renew these items
637     $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
638         {
639             borrowernumber => $hold_waiting_borrowernumber,
640             biblionumber   => $biblio->biblionumber,
641             itemnumber     => $item_3->itemnumber,
642             branchcode     => $branch,
643             priority       => 0,
644             found          => 'W'
645         }
646     );
647     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
648     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
649     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
650     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
651     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
652
653     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
654     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
655     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
656
657     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
658     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
659     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
660
661     my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
662     my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber );
663     AddIssue($reserving_borrower, $item_3->barcode);
664     my $reserve = $dbh->selectrow_hashref(
665         'SELECT * FROM old_reserves WHERE reserve_id = ?',
666         { Slice => {} },
667         $reserveid
668     );
669     is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
670
671     # Item-level hold, renewal test
672     AddReserve(
673         {
674             branchcode       => $branch,
675             borrowernumber   => $reserving_borrowernumber,
676             biblionumber     => $biblio->biblionumber,
677             priority         => $priority,
678             reservation_date => $resdate,
679             expiration_date  => $expdate,
680             notes            => $notes,
681             itemnumber       => $item_1->itemnumber,
682             found            => $found,
683         }
684     );
685
686     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1, 1);
687     is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
688     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
689
690     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2, 1);
691     is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
692
693     # Items can't fill hold for reasons
694     $issue_1->item->notforloan(1)->store;
695     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1, 1);
696     is( $renewokay, 0, 'Cannot renew, item is marked not for loan, but an item specific hold always blocks');
697     $item_1->set({notforloan => 0, itype => $itemtype })->store;
698
699     # FIXME: Add more for itemtype not for loan etc.
700
701     # Restricted users cannot renew when RestrictionBlockRenewing is enabled
702     my $item_5 = $builder->build_sample_item(
703         {
704             biblionumber     => $biblio->biblionumber,
705             library          => $branch,
706             replacementprice => 23.00,
707             itype            => $itemtype,
708         }
709     );
710     my $issue_5 = AddIssue($restricted_borrower_obj, $item_5->barcode);
711     is (defined $issue_5, 1, "Item with date due checked out, due date: ". $issue_5->date_due);
712
713     t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
714     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_2);
715     is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
716     ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrower_obj, $issue_5);
717     is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
718     is( $error, 'restriction', "Correct error returned");
719
720     # Users cannot renew an overdue item
721     my $item_6 = $builder->build_sample_item(
722         {
723             biblionumber     => $biblio->biblionumber,
724             library          => $branch,
725             replacementprice => 23.00,
726             itype            => $itemtype,
727         }
728     );
729
730     my $item_7 = $builder->build_sample_item(
731         {
732             biblionumber     => $biblio->biblionumber,
733             library          => $branch,
734             replacementprice => 23.00,
735             itype            => $itemtype,
736         }
737     );
738
739     my $issue_6 = AddIssue( $renewing_borrower_obj, $item_6->barcode);
740     is (defined $issue_6, 1, "Item 2 checked out, due date: ".$issue_6->date_due);
741
742     my $now = dt_from_string();
743     my $five_weeks = DateTime::Duration->new(weeks => 5);
744     my $five_weeks_ago = $now - $five_weeks;
745     t::lib::Mocks::mock_preference('finesMode', 'production');
746
747     my $issue_7 = AddIssue($renewing_borrower_obj, $item_7->barcode, $five_weeks_ago);
748     is (defined $issue_7, 1, "Item with passed date due checked out, due date: " . $issue_7->date_due);
749
750     t::lib::Mocks::mock_preference('OverduesBlockRenewing','allow');
751     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_6);
752     is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
753     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_7);
754     is( $renewokay, 1, '(Bug 8236), Can renew, this item is overdue but not pref does not block');
755
756     t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
757     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_6);
758     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is not overdue but patron has overdues');
759     is( $error, 'overdue', "Correct error returned");
760     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_7);
761     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue so patron has overdues');
762     is( $error, 'overdue', "Correct error returned");
763
764     t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
765     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_6);
766     is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
767     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_7);
768     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
769     is( $error, 'overdue', "Correct error returned");
770
771     my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower_obj->categorycode, $branch, $five_weeks_ago, $now );
772     C4::Overdues::UpdateFine(
773         {
774             issue_id       => $issue_7->id(),
775             itemnumber     => $item_7->itemnumber,
776             borrowernumber => $renewing_borrower_obj->borrowernumber,
777             amount         => $fine,
778             due            => Koha::DateUtils::output_pref($five_weeks_ago)
779         }
780     );
781
782     # Make sure fine calculation isn't skipped when adding renewal
783     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
784
785     # Calculate new due-date based on the present date not to incur
786     # multiple fees
787     t::lib::Mocks::mock_preference('RenewalPeriodBase', 'now');
788
789     my $staff = $builder->build_object({ class => "Koha::Patrons" });
790     t::lib::Mocks::mock_userenv({ patron => $staff });
791
792     t::lib::Mocks::mock_preference('RenewalLog', 0);
793     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
794     my %params_renewal = (
795         timestamp => { -like => $date . "%" },
796         module => "CIRCULATION",
797         action => "RENEWAL",
798     );
799     my %params_issue = (
800         timestamp => { -like => $date . "%" },
801         module => "CIRCULATION",
802         action => "ISSUE"
803     );
804     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );
805     my $dt = dt_from_string();
806     Time::Fake->offset( $dt->epoch );
807     my $datedue1 = AddRenewal(
808         {
809             borrowernumber => $renewing_borrower_obj->borrowernumber,
810             itemnumber     => $item_7->itemnumber,
811             branch         => $branch
812         }
813     );
814     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
815     is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
816     isnt (DateTime->compare($datedue1, $dt), 0, "AddRenewal returned a good duedate");
817     Time::Fake->reset;
818
819     t::lib::Mocks::mock_preference('RenewalLog', 1);
820     $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
821     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
822     AddRenewal(
823         {
824             borrowernumber => $renewing_borrower_obj->borrowernumber,
825             itemnumber     => $item_7->itemnumber,
826             branch         => $branch
827         }
828     );
829     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
830     is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
831
832     my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower_obj->borrowernumber, itemnumber => $item_7->itemnumber } );
833     is( $fines->count, 1, 'AddRenewal left fine' );
834     is( $fines->next->status, 'RENEWED', 'Fine on renewed item is closed out properly' );
835     $fines->delete();
836
837     my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
838     my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
839     AddIssue(
840         $renewing_borrower_obj,
841         $item_7->barcode,
842         Koha::DateUtils::output_pref({str=>$issue_6->date_due, dateformat =>'iso'}),
843         0,
844         $date,
845         0,
846         undef
847     ); # TODO: Already issued???
848     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
849     is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
850     $new_log_size = Koha::ActionLogs->count( \%params_issue );
851     is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
852
853     $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
854     $hold->cancel;
855
856     # Bug 14101
857     # Test automatic renewal before value for "norenewalbefore" in policy is set
858     # In this case automatic renewal should still be permitted
859     my $item_4 = $builder->build_sample_item(
860         {
861             biblionumber     => $biblio->biblionumber,
862             library          => $branch,
863             replacementprice => 16.00,
864             itype            => $itemtype,
865         }
866     );
867
868     my $auto_renew_issue =
869         AddIssue( $renewing_borrower_obj, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
870     my $info;
871     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
872     is( $info->{soonest_renew_date}, undef, "soonest_renew_date is not returned because this issue can be renewed" );
873     is( $renewokay, 1, 'Bug 25393: Can do a manual renew, even if renewal is automatic and premature' );
874     is(
875         $error, 'auto_renew',
876         'Bug 25393: Can do a manual renew, even if renewal is automatic and premature, norenewalbefore = undef -> manual renewals are allowed at any time'
877     );
878
879     my ( $renewokay_cron, $error_cron, $info_cron ) =
880         CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
881     is(
882         $info_cron->{soonest_renew_date}, dt_from_string( $auto_renew_issue->date_due ),
883         "Due date is returned as earliest renewal date when the CanBookBeRenewed check is done by cron and error is 'auto_too_soon'"
884     );
885     AddReserve(
886         {
887             branchcode       => $branch,
888             borrowernumber   => $reserving_borrowernumber,
889             biblionumber     => $biblio->biblionumber,
890             itemnumber       => $bibitems,
891             priority         => $priority,
892             reservation_date => $resdate,
893             expiration_date  => $expdate,
894             notes            => $notes,
895             title            => 'a title',
896             itemnumber       => $item_4->itemnumber,
897             found            => $found
898         }
899     );
900     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
901     is( $renewokay, 0, 'Still should not be able to renew' );
902     is( $error, 'on_reserve', 'returned code is on_reserve, reserve checked when not checking for cron' );
903     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
904     is( $renewokay, 0, 'Still should not be able to renew' );
905     is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked when checking for cron' );
906     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Due date is returned as earliest renewal date when error is 'auto_too_soon'" );
907     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, 1 );
908     is( $renewokay, 0, 'Still should not be able to renew' );
909     is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
910     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, 1, 1 );
911     is( $renewokay, 0, 'Still should not be able to renew' );
912     is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
913     $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
914     Koha::Cache::Memory::Lite->flush();
915     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, 1 );
916     is( $renewokay, 0, 'Still should not be able to renew' );
917     is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
918     ModReserveCancelAll($item_4->itemnumber, $reserving_borrowernumber);
919
920     $renewing_borrower_obj = Koha::Patrons->find($renewing_borrower_obj->borrowernumber);
921     $renewing_borrower_obj->autorenew_checkouts(0)->store;
922     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
923     is( $renewokay, 1, 'No renewal before is undef, but patron opted out of auto_renewal' );
924     $renewing_borrower_obj->autorenew_checkouts(1)->store;
925
926
927     # Bug 7413
928     # Test premature manual renewal
929     Koha::CirculationRules->set_rule(
930         {
931             categorycode => undef,
932             branchcode   => undef,
933             itemtype     => undef,
934             rule_name    => 'norenewalbefore',
935             rule_value   => '7',
936         }
937     );
938
939     ( $renewokay, $error, $info ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
940     is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
941     is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
942     is( $info->{soonest_renew_date}, dt_from_string($issue_1->date_due)->subtract( days => 7 ), "Soonest renew date returned when error is 'too_soon'");
943
944     # Bug 25393
945     # Test premature automatic renewal
946     Koha::CirculationRules->set_rule(
947         {
948             categorycode => undef,
949             branchcode   => undef,
950             itemtype     => undef,
951             rule_name    => 'noautorenewalbefore',
952             rule_value   => '7',
953         }
954     );
955
956     ( $renewokay, $error, $info ) =
957     CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
958     is( $renewokay, 0, 'Bug 25393: Cannot renew, renewal is premature' );
959     is( $error, 'too_soon',
960         'Bug 25393: Cannot renew, renewal is premature (returned code is too_soon)'
961     );
962     is(
963         $info->{soonest_renew_date}, dt_from_string( $auto_renew_issue->date_due )->subtract( days => 7 ),
964         "Soonest renew date returned when error is 'auto_too_soon'"
965     );
966
967     ( $renewokay_cron, $error_cron, $info_cron ) =
968         CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
969     is(
970         $error_cron, 'auto_too_soon',
971         'Bug 25393: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
972     );
973     $renewing_borrower_obj->autorenew_checkouts(0)->store;
974     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
975     is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
976     is( $error, 'too_soon', 'Error is too_soon, no auto' );
977     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due)->subtract( days => 7 ), "Soonest renew date returned when error is 'too_soon'");
978     $renewing_borrower_obj->autorenew_checkouts(1)->store;
979
980     # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
981     # and test automatic renewal again
982     $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
983     Koha::Cache::Memory::Lite->flush();
984     ( $renewokay, $error, $info ) =
985       CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
986     is( $renewokay, 0, 'Bug 25393: Cannot manual renew, renewal is too soon' );
987     is( $error, 'too_soon',
988         'Bug 25393: Cannot manual renew, renewal is too soon, even though it\'s also scheduled for auto renewal'
989     );
990     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
991
992     # Now we test for noautorenewalbefore if cron
993     $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'noautorenewalbefore'});
994     Koha::Cache::Memory::Lite->flush();
995     ( $renewokay, $error, $info ) =
996       CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
997     is( $renewokay, 0, 'Bug 25393: Cannot auto renew, renewal is too soon' );
998     is( $error, 'auto_too_soon',
999         'Bug 25393: Cannot auto renew, auto renewal is too soon'
1000     );
1001     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
1002
1003     $renewing_borrower_obj->autorenew_checkouts(0)->store;
1004     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
1005     is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
1006     is( $error, 'too_soon', 'Error is too_soon, no auto' );
1007     is( $info->{soonest_renew_date}, dt_from_string($auto_renew_issue->date_due), "Soonest renew date returned when error is 'auto_too_soon'");
1008     $renewing_borrower_obj->autorenew_checkouts(1)->store;
1009
1010     # Change policy so that loans can be renewed 99 days prior to the due date
1011     # and test automatic renewal again
1012     $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
1013     Koha::Cache::Memory::Lite->flush();
1014     ( $renewokay, $error ) =
1015       CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
1016     is( $renewokay, 1, 'Bug 25393: Can do a manual renew, even if renewal is automatic and premature' );
1017     is( $error, 'auto_renew',
1018         'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
1019     );
1020
1021     # Now we test for noautorenewalbefore if cron
1022     $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'noautorenewalbefore'});
1023     Koha::Cache::Memory::Lite->flush();
1024     ( $renewokay, $error, $info ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue, undef, 1 );
1025     is( $renewokay, 1, 'Bug 25393: Can auto renew' );
1026     is( $error, 'auto_renew', 'Bug 25393: Can auto renew' );
1027     is( $info->{soonest_renew_date}, undef, "soonest_renew_date is not returned because this issue can be renewed" );
1028
1029     $renewing_borrower_obj->autorenew_checkouts(0)->store;
1030     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $auto_renew_issue );
1031     is( $renewokay, 1, 'No renewal before is 99, patron opted out of auto_renewal so can renew' );
1032     $renewing_borrower_obj->autorenew_checkouts(1)->store;
1033
1034
1035     # Bug 31427
1036     # Ensure autorenewal errors always take highest precedence
1037     subtest "auto_renewal errors first" => sub {
1038         plan tests => 4;
1039
1040         my $auto_renew_item = $builder->build_sample_item(
1041             {
1042                 biblionumber => $biblio->biblionumber,
1043                 library      => $branch,
1044             }
1045         );
1046
1047         my $ten_days_ahead = dt_from_string->add( days => 10 );
1048         my $issue          = AddIssue(
1049             $renewing_borrower_obj, $auto_renew_item->barcode, $ten_days_ahead, undef, undef, undef,
1050             { auto_renew => 1 }
1051         );
1052
1053         Koha::CirculationRules->set_rules(
1054             {
1055                 categorycode => undef,
1056                 branchcode   => undef,
1057                 itemtype     => undef,
1058                 rules        => {
1059                     noautorenewalbefore => 7,
1060                     renewalsallowed     => 2,
1061                 }
1062             }
1063         );
1064         my ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue, undef, 'cron' );
1065         is( $renewokay, 0,               'Do not renew, renewal is automatic' );
1066         is( $error,     'auto_too_soon', 'Cannot auto renew, too soon - returned code is auto_too_soon' );
1067
1068         $issue->renewals_count(2)->store;
1069         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue, undef, 'cron' );
1070         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1071         is(
1072             $error, 'auto_too_soon',
1073             'Cannot auto renew, too soon - auto renewal error takes precedence over non-autorenewal error too_many'
1074         );
1075     };
1076
1077     subtest "too_late_renewal / no_auto_renewal_after" => sub {
1078         plan tests => 16;
1079         my $item_to_auto_renew = $builder->build_sample_item(
1080             {
1081                 biblionumber => $biblio->biblionumber,
1082                 library      => $branch,
1083             }
1084         );
1085
1086         my $ten_days_before = dt_from_string->add( days => -10 );
1087         my $ten_days_ahead  = dt_from_string->add( days => 10 );
1088         my $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1089
1090         Koha::CirculationRules->set_rules(
1091             {
1092                 categorycode => undef,
1093                 branchcode   => undef,
1094                 itemtype     => undef,
1095                 rules        => {
1096                     norenewalbefore       => '7',
1097                     no_auto_renewal_after => '9',
1098                 }
1099             }
1100         );
1101         ( $renewokay, $error ) =
1102           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1103         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1104         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
1105
1106         Koha::CirculationRules->set_rules(
1107             {
1108                 categorycode => undef,
1109                 branchcode   => undef,
1110                 itemtype     => undef,
1111                 rules        => {
1112                     norenewalbefore       => '7',
1113                     no_auto_renewal_after => '10',
1114                 }
1115             }
1116         );
1117         ( $renewokay, $error ) =
1118           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1119         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1120         is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
1121
1122         Koha::CirculationRules->set_rules(
1123             {
1124                 categorycode => undef,
1125                 branchcode   => undef,
1126                 itemtype     => undef,
1127                 rules        => {
1128                     norenewalbefore       => '7',
1129                     no_auto_renewal_after => '11',
1130                 }
1131             }
1132         );
1133         ( $renewokay, $error ) =
1134           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1135         is( $renewokay, 0, 'Do not renew, too_soon' );
1136         is( $error, 'too_soon', 'Cannot renew, too_soon - no_auto_renewal_after is defined(returned code is too_soon)' );
1137
1138         # Now check for cron
1139         Koha::CirculationRules->set_rules(
1140             {
1141                 categorycode => undef,
1142                 branchcode   => undef,
1143                 itemtype     => undef,
1144                 rules        => {
1145                     noautorenewalbefore   => '9',
1146                     no_auto_renewal_after => '11',
1147                 }
1148             }
1149         );
1150         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue, undef, 1 );
1151         is( $renewokay, 0, 'Do not renew, too_soon' );
1152         is(
1153             $error, 'auto_too_soon',
1154             'Cannot renew, too_soon - no_auto_renewal_after is defined(returned code is too_soon)'
1155         );
1156
1157         Koha::CirculationRules->set_rules(
1158             {
1159                 categorycode => undef,
1160                 branchcode   => undef,
1161                 itemtype     => undef,
1162                 rules        => {
1163                     norenewalbefore       => '10',
1164                     no_auto_renewal_after => '11',
1165                 }
1166             }
1167         );
1168         ( $renewokay, $error ) =
1169           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1170         is( $renewokay, 1,            'Renew, even if renewal is automatic' );
1171         is( $error,     'auto_renew', 'Cannot renew, renew is automatic' );
1172
1173         Koha::CirculationRules->set_rules(
1174             {
1175                 categorycode => undef,
1176                 branchcode   => undef,
1177                 itemtype     => undef,
1178                 rules        => {
1179                     norenewalbefore       => '10',
1180                     no_auto_renewal_after => undef,
1181                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
1182                 }
1183             }
1184         );
1185         ( $renewokay, $error ) =
1186           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1187         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1188         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
1189
1190         Koha::CirculationRules->set_rules(
1191             {
1192                 categorycode => undef,
1193                 branchcode   => undef,
1194                 itemtype     => undef,
1195                 rules        => {
1196                     norenewalbefore       => '7',
1197                     no_auto_renewal_after => '15',
1198                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
1199                 }
1200             }
1201         );
1202         ( $renewokay, $error ) =
1203           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1204         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1205         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
1206
1207         Koha::CirculationRules->set_rules(
1208             {
1209                 categorycode => undef,
1210                 branchcode   => undef,
1211                 itemtype     => undef,
1212                 rules        => {
1213                     norenewalbefore       => '10',
1214                     no_auto_renewal_after => undef,
1215                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 1 ),
1216                 }
1217             }
1218         );
1219         ( $renewokay, $error ) =
1220           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1221         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1222         is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
1223     };
1224
1225     subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew & OPACFineNoRenewalsIncludeCredit" => sub {
1226         plan tests => 10;
1227         my $item_to_auto_renew = $builder->build_sample_item(
1228             {
1229                 biblionumber => $biblio->biblionumber,
1230                 library      => $branch,
1231             }
1232         );
1233
1234         my $ten_days_before = dt_from_string->add( days => -10 );
1235         my $ten_days_ahead = dt_from_string->add( days => 10 );
1236         my $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1237
1238         Koha::CirculationRules->set_rules(
1239             {
1240                 categorycode => undef,
1241                 branchcode   => undef,
1242                 itemtype     => undef,
1243                 rules        => {
1244                     norenewalbefore       => '10',
1245                     no_auto_renewal_after => '11',
1246                 }
1247             }
1248         );
1249         C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
1250         C4::Context->set_preference('OPACFineNoRenewals','10');
1251         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
1252         my $fines_amount = 5;
1253         my $account = Koha::Account->new({patron_id => $renewing_borrowernumber});
1254         $account->add_debit(
1255             {
1256                 amount      => $fines_amount,
1257                 interface   => 'test',
1258                 type        => 'OVERDUE',
1259                 item_id     => $item_to_auto_renew->itemnumber,
1260                 description => "Some fines"
1261             }
1262         )->status('RETURNED')->store;
1263         ( $renewokay, $error ) =
1264           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1265         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1266         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
1267
1268         $account->add_debit(
1269             {
1270                 amount      => $fines_amount,
1271                 interface   => 'test',
1272                 type        => 'OVERDUE',
1273                 item_id     => $item_to_auto_renew->itemnumber,
1274                 description => "Some fines"
1275             }
1276         )->status('RETURNED')->store;
1277         ( $renewokay, $error ) =
1278           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1279         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1280         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
1281
1282         $account->add_debit(
1283             {
1284                 amount      => $fines_amount,
1285                 interface   => 'test',
1286                 type        => 'OVERDUE',
1287                 item_id     => $item_to_auto_renew->itemnumber,
1288                 description => "Some fines"
1289             }
1290         )->status('RETURNED')->store;
1291         ( $renewokay, $error ) =
1292           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1293         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1294         is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
1295
1296         $account->add_credit(
1297             {
1298                 amount      => $fines_amount,
1299                 interface   => 'test',
1300                 type        => 'PAYMENT',
1301                 description => "Some payment"
1302             }
1303         )->store;
1304         ( $renewokay, $error ) =
1305           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1306         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1307         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit'  );
1308
1309         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','0');
1310         ( $renewokay, $error ) =
1311           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1312         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1313         is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit'  );
1314
1315         $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
1316         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
1317     };
1318
1319     subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
1320         plan tests => 6;
1321         my $item_to_auto_renew = $builder->build_sample_item(
1322             {
1323                 biblionumber => $biblio->biblionumber,
1324                 library      => $branch,
1325             }
1326         );
1327
1328         Koha::CirculationRules->set_rules(
1329             {
1330                 categorycode => undef,
1331                 branchcode   => undef,
1332                 itemtype     => undef,
1333                 rules        => {
1334                     norenewalbefore       => 10,
1335                     no_auto_renewal_after => 11,
1336                 }
1337             }
1338         );
1339
1340         my $ten_days_before = dt_from_string->add( days => -10 );
1341         my $ten_days_ahead = dt_from_string->add( days => 10 );
1342
1343         # Patron is expired and BlockExpiredPatronOpacActions=0
1344         # => auto renew is allowed
1345         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
1346         my $issue = AddIssue( $expired_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1347         ( $renewokay, $error ) =
1348           CanBookBeRenewed( $expired_borrower_obj, $issue );
1349         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1350         is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
1351         Koha::Checkouts->find( $issue->issue_id )->delete;
1352
1353
1354         # Patron is expired and BlockExpiredPatronOpacActions=1
1355         # => auto renew is not allowed
1356         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1357         $issue = AddIssue( $expired_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1358         ( $renewokay, $error ) =
1359           CanBookBeRenewed( $expired_borrower_obj, $issue );
1360         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1361         is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
1362         $issue->delete;
1363
1364         # Patron is not expired and BlockExpiredPatronOpacActions=1
1365         # => auto renew is allowed
1366         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1367         $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1368         ( $renewokay, $error ) =
1369           CanBookBeRenewed( $renewing_borrower_obj, $issue );
1370         is( $renewokay, 1, 'Renew, even if renewal is automatic' );
1371         is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
1372         $issue->delete;
1373     };
1374
1375     subtest "GetLatestAutoRenewDate" => sub {
1376         plan tests => 5;
1377         my $item_to_auto_renew = $builder->build_sample_item(
1378             {
1379                 biblionumber => $biblio->biblionumber,
1380                 library      => $branch,
1381             }
1382         );
1383
1384         my $ten_days_before = dt_from_string->add( days => -10 );
1385         my $ten_days_ahead  = dt_from_string->add( days => 10 );
1386         my $issue = AddIssue( $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1387         Koha::CirculationRules->set_rules(
1388             {
1389                 categorycode => undef,
1390                 branchcode   => undef,
1391                 itemtype     => undef,
1392                 rules        => {
1393                     norenewalbefore       => '7',
1394                     no_auto_renewal_after => '',
1395                     no_auto_renewal_after_hard_limit => undef,
1396                 }
1397             }
1398         );
1399         my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1400         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' );
1401         my $five_days_before = dt_from_string->add( days => -5 );
1402         Koha::CirculationRules->set_rules(
1403             {
1404                 categorycode => undef,
1405                 branchcode   => undef,
1406                 itemtype     => undef,
1407                 rules        => {
1408                     norenewalbefore       => '10',
1409                     no_auto_renewal_after => '5',
1410                     no_auto_renewal_after_hard_limit => undef,
1411                 }
1412             }
1413         );
1414         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj,, $issue );
1415         is( $latest_auto_renew_date->truncate( to => 'minute' ),
1416             $five_days_before->truncate( to => 'minute' ),
1417             'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
1418         );
1419         my $five_days_ahead = dt_from_string->add( days => 5 );
1420         $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
1421         $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
1422         $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
1423         Koha::Cache::Memory::Lite->flush();
1424         Koha::CirculationRules->set_rules(
1425             {
1426                 categorycode => undef,
1427                 branchcode   => undef,
1428                 itemtype     => undef,
1429                 rules        => {
1430                     norenewalbefore       => '10',
1431                     no_auto_renewal_after => '15',
1432                     no_auto_renewal_after_hard_limit => undef,
1433                 }
1434             }
1435         );
1436         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1437         is( $latest_auto_renew_date->truncate( to => 'minute' ),
1438             $five_days_ahead->truncate( to => 'minute' ),
1439             'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
1440         );
1441         my $two_days_ahead = dt_from_string->add( days => 2 );
1442         Koha::CirculationRules->set_rules(
1443             {
1444                 categorycode => undef,
1445                 branchcode   => undef,
1446                 itemtype     => undef,
1447                 rules        => {
1448                     norenewalbefore       => '10',
1449                     no_auto_renewal_after => '',
1450                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1451                 }
1452             }
1453         );
1454         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1455         is( $latest_auto_renew_date->truncate( to => 'day' ),
1456             $two_days_ahead->truncate( to => 'day' ),
1457             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
1458         );
1459         Koha::CirculationRules->set_rules(
1460             {
1461                 categorycode => undef,
1462                 branchcode   => undef,
1463                 itemtype     => undef,
1464                 rules        => {
1465                     norenewalbefore       => '10',
1466                     no_auto_renewal_after => '15',
1467                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1468                 }
1469             }
1470         );
1471         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrower_obj, $issue );
1472         is( $latest_auto_renew_date->truncate( to => 'day' ),
1473             $two_days_ahead->truncate( to => 'day' ),
1474             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
1475         );
1476
1477     };
1478
1479     subtest "auto_renew_final" => sub {
1480         plan tests => 6;
1481         my $item_to_auto_renew = $builder->build_sample_item();
1482         Koha::CirculationRules->set_rules(
1483             {
1484                 categorycode => undef,
1485                 branchcode   => undef,
1486                 itemtype     => $item_to_auto_renew->itype,
1487                 rules        => {
1488                     norenewalbefore => undef,
1489                     renewalsallowed => 1,
1490                 }
1491             }
1492         );
1493
1494         my $ten_days_before = dt_from_string->add( days => -10 );
1495         my $ten_days_ahead  = dt_from_string->add( days => 10 );
1496         my $issue           = AddIssue(
1497             $renewing_borrower_obj, $item_to_auto_renew->barcode, $ten_days_ahead, undef, $ten_days_before,
1498             undef, { auto_renew => 1 }
1499         );
1500         $issue->renewals_count(0)->store;
1501
1502         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue );
1503         is( $renewokay, 1,                  'Success for auto_renewal' );
1504         is( $error,     'auto_renew_final', 'Auto renewal allowed, but it is the last one' );
1505
1506         # Final unseen renewal
1507         Koha::CirculationRules->set_rules(
1508             {
1509                 categorycode => undef,
1510                 branchcode   => undef,
1511                 itemtype     => $item_to_auto_renew->itype,
1512                 rules        => {
1513                     unseen_renewals_allowed => 2,
1514                     renewalsallowed         => 10,
1515                 }
1516             }
1517         );
1518         t::lib::Mocks::mock_preference( 'UnseenRenewals', 1 );
1519         $issue->unseen_renewals(1)->renewals_count(1)->store;
1520
1521         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue );
1522         is( $renewokay, 1,                   'Success for auto_renewal' );
1523         is( $error,     'auto_unseen_final', 'Final unseen renewal reported, not final overall' );
1524
1525         # Final unseen renewal
1526         Koha::CirculationRules->set_rules(
1527             {
1528                 categorycode => undef,
1529                 branchcode   => undef,
1530                 itemtype     => $item_to_auto_renew->itype,
1531                 rules        => {
1532                     unseen_renewals_allowed => 2,
1533                     renewalsallowed         => 2,
1534                 }
1535             }
1536         );
1537         ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $issue );
1538         is( $renewokay, 1,                  'Success for auto_renewal' );
1539         is( $error,     'auto_renew_final', 'Final auto renewal reported' );
1540
1541     };
1542
1543     # Too many renewals
1544     # set policy to forbid renewals
1545     Koha::CirculationRules->set_rules(
1546         {
1547             categorycode => undef,
1548             branchcode   => undef,
1549             itemtype     => undef,
1550             rules        => {
1551                 norenewalbefore => undef,
1552                 renewalsallowed => 0,
1553             }
1554         }
1555     );
1556
1557     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
1558     is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
1559     is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
1560
1561     # Too many unseen renewals
1562     Koha::CirculationRules->set_rules(
1563         {
1564             categorycode => undef,
1565             branchcode   => undef,
1566             itemtype     => undef,
1567             rules        => {
1568                 unseen_renewals_allowed => 2,
1569                 renewalsallowed => 10,
1570             }
1571         }
1572     );
1573     t::lib::Mocks::mock_preference('UnseenRenewals', 1);
1574
1575     $issue_1->unseen_renewals(2)->store;
1576
1577     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrower_obj, $issue_1);
1578     is( $renewokay, 0, 'Cannot renew, 0 unseen renewals allowed');
1579     is( $error, 'too_unseen', 'Cannot renew, returned code is too_unseen');
1580     Koha::CirculationRules->set_rules(
1581         {
1582             categorycode => undef,
1583             branchcode   => undef,
1584             itemtype     => undef,
1585             rules        => {
1586                 norenewalbefore => undef,
1587                 renewalsallowed => 0,
1588             }
1589         }
1590     );
1591     t::lib::Mocks::mock_preference('UnseenRenewals', 0);
1592
1593     # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
1594     t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
1595     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1596
1597     C4::Overdues::UpdateFine(
1598         {
1599             issue_id       => $issue_1->id(),
1600             itemnumber     => $item_1->itemnumber,
1601             borrowernumber => $renewing_borrower_obj->borrowernumber,
1602             amount         => 15.00,
1603             type           => q{},
1604             due            => Koha::DateUtils::output_pref($datedue)
1605         }
1606     );
1607
1608     my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower_obj->borrowernumber })->next();
1609     is( $line->debit_type_code, 'OVERDUE', 'Account line type is OVERDUE' );
1610     is( $line->status, 'UNRETURNED', 'Account line status is UNRETURNED' );
1611     is( $line->amountoutstanding+0, 15, 'Account line amount outstanding is 15.00' );
1612     is( $line->amount+0, 15, 'Account line amount is 15.00' );
1613     is( $line->issue_id, $issue_1->id, 'Account line issue id matches' );
1614
1615     my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
1616     is( $offset->type, 'CREATE', 'Account offset type is CREATE' );
1617     is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
1618
1619     t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
1620     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
1621
1622     LostItem( $item_1->itemnumber, 'test', 1 );
1623
1624     $line = Koha::Account::Lines->find($line->id);
1625     is( $line->debit_type_code, 'OVERDUE', 'Account type remains as OVERDUE' );
1626     isnt( $line->status, 'UNRETURNED', 'Account status correctly changed from UNRETURNED to RETURNED' );
1627
1628     my $item = Koha::Items->find($item_1->itemnumber);
1629     ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
1630     my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
1631     is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
1632
1633     my $total_due = $dbh->selectrow_array(
1634         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1635         undef, $renewing_borrower_obj->borrowernumber
1636     );
1637
1638     is( $total_due+0, 15, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
1639
1640     C4::Context->dbh->do("DELETE FROM accountlines");
1641
1642     C4::Overdues::UpdateFine(
1643         {
1644             issue_id       => $issue_2->id(),
1645             itemnumber     => $item_2->itemnumber,
1646             borrowernumber => $renewing_borrower_obj->borrowernumber,
1647             amount         => 15.00,
1648             type           => q{},
1649             due            => Koha::DateUtils::output_pref($datedue)
1650         }
1651     );
1652
1653     LostItem( $item_2->itemnumber, 'test', 0 );
1654
1655     my $item2 = Koha::Items->find($item_2->itemnumber);
1656     ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
1657     ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
1658
1659     $total_due = $dbh->selectrow_array(
1660         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1661         undef, $renewing_borrower_obj->borrowernumber
1662     );
1663
1664     ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
1665
1666     my $future = dt_from_string();
1667     $future->add( days => 7 );
1668     my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
1669     ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
1670
1671     my $manager = $builder->build_object({ class => "Koha::Patrons" });
1672     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
1673     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1674     $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
1675     LostItem( $item_3->itemnumber, 'test', 0 );
1676     my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
1677     is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
1678     is(
1679         $accountline->description,
1680         sprintf( "%s %s %s",
1681             $item_3->biblio->title  || '',
1682             $item_3->barcode        || '',
1683             $item_3->itemcallnumber || '' ),
1684         "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1685     );
1686
1687     # Recalls
1688     t::lib::Mocks::mock_preference('UseRecalls', 1);
1689     Koha::CirculationRules->set_rules({
1690         categorycode => undef,
1691         branchcode => undef,
1692         itemtype => undef,
1693         rules => {
1694             recalls_allowed => 10,
1695             renewalsallowed => 5,
1696         },
1697     });
1698     my $recall_borrower = $builder->build_object({ class => 'Koha::Patrons' });
1699     my $recall_biblio = $builder->build_sample_biblio();
1700     my $recall_item1 = $builder->build_sample_item({ biblionumber => $recall_biblio->biblionumber });
1701     my $recall_item2 = $builder->build_sample_item({ biblionumber => $recall_biblio->biblionumber });
1702
1703     my $recall_issue = AddIssue( $renewing_borrower_obj, $recall_item1->barcode );
1704
1705     # item-level and this item: renewal not allowed
1706     my $recall = Koha::Recall->new({
1707         biblio_id => $recall_item1->biblionumber,
1708         item_id => $recall_item1->itemnumber,
1709         patron_id => $recall_borrower->borrowernumber,
1710         pickup_library_id => $recall_borrower->branchcode,
1711         item_level => 1,
1712     })->store;
1713     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1714     is( $error, 'recalled', 'Cannot renew item that has been recalled' );
1715     $recall->set_cancelled;
1716
1717     # biblio-level requested recall: renewal not allowed
1718     $recall = Koha::Recall->new({
1719         biblio_id => $recall_item1->biblionumber,
1720         item_id => undef,
1721         patron_id => $recall_borrower->borrowernumber,
1722         pickup_library_id => $recall_borrower->branchcode,
1723         item_level => 0,
1724     })->store;
1725     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1726     is( $error, 'recalled', 'Cannot renew item if biblio is recalled and has no item allocated' );
1727     $recall->set_cancelled;
1728
1729     # item-level and not this item: renewal allowed
1730     $recall = Koha::Recall->new({
1731         biblio_id => $recall_item2->biblionumber,
1732         item_id => $recall_item2->itemnumber,
1733         patron_id => $recall_borrower->borrowernumber,
1734         pickup_library_id => $recall_borrower->branchcode,
1735         item_level => 1,
1736     })->store;
1737     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1738     is( $renewokay, 1, 'Can renew item if item-level recall on biblio is not on this item' );
1739     $recall->set_cancelled;
1740
1741     # biblio-level waiting recall: renewal allowed
1742     $recall = Koha::Recall->new({
1743         biblio_id => $recall_item1->biblionumber,
1744         item_id => undef,
1745         patron_id => $recall_borrower->borrowernumber,
1746         pickup_library_id => $recall_borrower->branchcode,
1747         item_level => 0,
1748     })->store;
1749     $recall->set_waiting({ item => $recall_item1 });
1750     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrower_obj, $recall_issue );
1751     is( $renewokay, 1, 'Can renew item if biblio-level recall has already been allocated an item' );
1752     $recall->set_cancelled;
1753 };
1754
1755 subtest "CanBookBeRenewed | bookings" => sub {
1756     plan tests => 3;
1757
1758     my $schema = Koha::Database->schema;
1759     $schema->storage->txn_begin;
1760
1761     t::lib::Mocks::mock_preference( 'RenewalPeriodBase', 'date_due' );
1762
1763     my $renewing_patron = $builder->build_object( { class => 'Koha::Patrons' } );
1764     my $booked_patron   = $builder->build_object( { class => 'Koha::Patrons' } );
1765     my $item            = $builder->build_sample_item( { bookable => 1 } );
1766
1767     # issue
1768     my $issue   = AddIssue( $renewing_patron, $item->barcode );
1769     my $datedue = dt_from_string( $issue->date_due() );
1770     is( defined $issue->date_due(), 1, "Item checked out, due date: " . $issue->date_due() );
1771
1772     # item-level booking
1773     my $booking = Koha::Booking->new(
1774         {
1775             patron_id  => $booked_patron->borrowernumber,
1776             item_id    => $item->itemnumber,
1777             biblio_id  => $item->biblio->biblionumber,
1778             start_date => $datedue->clone()->add( days => 2 ),
1779             end_date   => $datedue->clone()->add( days => 10 ),
1780         }
1781     )->store();
1782
1783     # Proposed renewal would encroach on booking
1784     my ( $renewok, $error ) = CanBookBeRenewed( $renewing_patron, $issue, 0 );
1785     is( $renewok, 0,        "Renewal not allowed as it would mean the item was not returned before the next booking" );
1786     is( $error,   'booked', "Error is 'booked'" );
1787
1788     $schema->storage->txn_rollback;
1789 };
1790
1791 subtest "GetUpcomingDueIssues" => sub {
1792     plan tests => 12;
1793
1794     my $branch   = $library2->{branchcode};
1795
1796     #Create another record
1797     my $biblio2 = $builder->build_sample_biblio();
1798
1799     #Create third item
1800     my $item_1 = Koha::Items->find($reused_itemnumber_1);
1801     my $item_2 = Koha::Items->find($reused_itemnumber_2);
1802     my $item_3 = $builder->build_sample_item(
1803         {
1804             biblionumber     => $biblio2->biblionumber,
1805             library          => $branch,
1806             itype            => $itemtype,
1807         }
1808     );
1809
1810
1811     # Create a borrower
1812     my %a_borrower_data = (
1813         firstname =>  'Fridolyn',
1814         surname => 'SOMERS',
1815         categorycode => $patron_category->{categorycode},
1816         branchcode => $branch,
1817     );
1818
1819     my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1820     my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber );
1821
1822     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1823     my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1824     my $today = DateTime->today(time_zone => C4::Context->tz());
1825
1826     my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1827     my $datedue = dt_from_string( $issue->date_due() );
1828     my $issue_2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1829     my $datedue2 = dt_from_string( $issue->date_due() );
1830
1831     my $upcoming_dues;
1832
1833     # GetUpcomingDueIssues tests
1834     for my $i(0..1) {
1835         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1836         is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1837     }
1838
1839     #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1840     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1841     is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1842
1843     for my $i(3..5) {
1844         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1845         is ( scalar( @$upcoming_dues ), 1,
1846             "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1847     }
1848
1849     # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1850
1851     my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1852
1853     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1854     is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1855
1856     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1857     is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1858
1859     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1860     is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1861
1862     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 }  );
1863     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1864
1865     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1866     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1867
1868     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1869     is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1870
1871 };
1872
1873 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1874     my $branch   = $library2->{branchcode};
1875
1876     my $biblio = $builder->build_sample_biblio();
1877
1878     #Create third item
1879     my $item = $builder->build_sample_item(
1880         {
1881             biblionumber     => $biblio->biblionumber,
1882             library          => $branch,
1883             itype            => $itemtype,
1884         }
1885     );
1886
1887     # Create a borrower
1888     my %a_borrower_data = (
1889         firstname =>  'Kyle',
1890         surname => 'Hall',
1891         categorycode => $patron_category->{categorycode},
1892         branchcode => $branch,
1893     );
1894
1895     my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1896
1897     my $borrower = Koha::Patrons->find( $borrowernumber );
1898     my $issue = AddIssue( $borrower, $item->barcode );
1899     UpdateFine(
1900         {
1901             issue_id       => $issue->id(),
1902             itemnumber     => $item->itemnumber,
1903             borrowernumber => $borrowernumber,
1904             amount         => 0,
1905             type           => q{}
1906         }
1907     );
1908
1909     my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1910     my $count = $hr->{count};
1911
1912     is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1913 };
1914
1915 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1916     plan tests => 13;
1917     my $biblio = $builder->build_sample_biblio();
1918     my $item_1 = $builder->build_sample_item(
1919         {
1920             biblionumber     => $biblio->biblionumber,
1921             library          => $library2->{branchcode},
1922         }
1923     );
1924     my $item_2= $builder->build_sample_item(
1925         {
1926             biblionumber     => $biblio->biblionumber,
1927             library          => $library2->{branchcode},
1928             itype            => $item_1->effective_itemtype,
1929         }
1930     );
1931
1932     Koha::CirculationRules->set_rules(
1933         {
1934             categorycode => undef,
1935             itemtype     => $item_1->effective_itemtype,
1936             branchcode   => undef,
1937             rules        => {
1938                 reservesallowed => 25,
1939                 holds_per_record => 25,
1940                 issuelength     => 14,
1941                 lengthunit      => 'days',
1942                 renewalsallowed => 1,
1943                 renewalperiod   => 7,
1944                 norenewalbefore => undef,
1945                 auto_renew      => 0,
1946                 fine            => .10,
1947                 chargeperiod    => 1,
1948                 maxissueqty     => 20
1949             }
1950         }
1951     );
1952
1953     my $borrower1 = $builder->build_object(
1954         {
1955             class => 'Koha::Patrons',
1956             value => {
1957                 firstname    => 'Kyle',
1958                 surname      => 'Hall',
1959                 categorycode => $patron_category->{categorycode},
1960                 branchcode   => $library2->{branchcode},
1961             }
1962         }
1963     );
1964
1965     my $borrowernumber2 = $builder->build_object(
1966         {
1967             class => 'Koha::Patrons',
1968             value => {
1969                 firstname    => 'Chelsea',
1970                 surname      => 'Hall',
1971                 categorycode => $patron_category->{categorycode},
1972                 branchcode   => $library2->{branchcode},
1973
1974             }
1975         }
1976     )->borrowernumber;
1977
1978     my $patron_category_2 = $builder->build(
1979         {
1980             source => 'Category',
1981             value  => {
1982                 category_type                 => 'P',
1983                 enrolmentfee                  => 0,
1984                 BlockExpiredPatronOpacActions => -1, # Pick the pref value
1985             }
1986         }
1987     );
1988
1989     my $borrowernumber3 = $builder->build_object(
1990         {
1991             class => 'Koha::Patrons',
1992             value => {
1993                 firstname    => 'Carnegie',
1994                 surname      => 'Hall',
1995                 categorycode => $patron_category_2->{categorycode},
1996                 branchcode   => $library2->{branchcode},
1997             }
1998         }
1999     )->borrowernumber;
2000
2001     my $issue = AddIssue( $borrower1, $item_1->barcode );
2002
2003     my ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2004     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
2005
2006     AddReserve(
2007         {
2008             branchcode     => $library2->{branchcode},
2009             borrowernumber => $borrowernumber2,
2010             biblionumber   => $biblio->biblionumber,
2011             priority       => 1,
2012         }
2013     );
2014
2015     Koha::CirculationRules->set_rules(
2016         {
2017             categorycode => undef,
2018             itemtype     => $item_1->effective_itemtype,
2019             branchcode   => undef,
2020             rules        => {
2021                 onshelfholds => 0,
2022             }
2023         }
2024     );
2025     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
2026     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2027     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
2028
2029     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
2030     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2031     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
2032
2033     Koha::CirculationRules->set_rules(
2034         {
2035             categorycode => undef,
2036             itemtype     => $item_1->effective_itemtype,
2037             branchcode   => undef,
2038             rules        => {
2039                 onshelfholds => 1,
2040             }
2041         }
2042     );
2043     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
2044     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2045     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
2046
2047     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
2048     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2049     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
2050
2051     AddReserve(
2052         {
2053             branchcode     => $library2->{branchcode},
2054             borrowernumber => $borrowernumber3,
2055             biblionumber   => $biblio->biblionumber,
2056             priority       => 1,
2057         }
2058     );
2059
2060     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2061     is( $renewokay, 0, 'Verify the borrower cannot renew with 2 holds on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and one other item on record' );
2062
2063     my $item_3= $builder->build_sample_item(
2064         {
2065             biblionumber     => $biblio->biblionumber,
2066             library          => $library2->{branchcode},
2067             itype            => $item_1->effective_itemtype,
2068         }
2069     );
2070
2071     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2072     is( $renewokay, 1, 'Verify the borrower cannot renew with 2 holds on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and two other items on record' );
2073
2074     Koha::CirculationRules->set_rules(
2075         {
2076             categorycode => $patron_category_2->{categorycode},
2077             itemtype     => $item_1->effective_itemtype,
2078             branchcode   => undef,
2079             rules        => {
2080                 reservesallowed => 0,
2081             }
2082         }
2083     );
2084
2085     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2086     is( $renewokay, 0, 'Verify the borrower cannot renew with 2 holds on the record, but only one of those holds can be filled when AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled and two other items on record' );
2087
2088     Koha::CirculationRules->set_rules(
2089         {
2090             categorycode => $patron_category_2->{categorycode},
2091             itemtype     => $item_1->effective_itemtype,
2092             branchcode   => undef,
2093             rules        => {
2094                 reservesallowed => 25,
2095             }
2096         }
2097     );
2098
2099     # Setting item not checked out to be not for loan but holdable
2100     $item_2->notforloan(-1)->store;
2101
2102     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2103     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' );
2104
2105     my $mock_circ = Test::MockModule->new("C4::Circulation");
2106     $mock_circ->mock( CanItemBeReserved => sub {
2107         warn "Checked";
2108         return { status => 'no' }
2109     } );
2110
2111     $item_2->notforloan(0)->store;
2112     $item_3->delete();
2113     # Two items total, one item available, one issued, two holds on record
2114
2115     warnings_are{
2116        ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2117     } [], "CanItemBeReserved not called when there are more possible holds than available items";
2118     is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
2119
2120     $item_3 = $builder->build_sample_item(
2121         {
2122             biblionumber     => $biblio->biblionumber,
2123             library          => $library2->{branchcode},
2124             itype            => $item_1->effective_itemtype,
2125         }
2126     );
2127
2128     Koha::CirculationRules->set_rules(
2129         {
2130             categorycode => undef,
2131             itemtype     => $item_1->effective_itemtype,
2132             branchcode   => undef,
2133             rules        => {
2134                 reservesallowed => 0,
2135             }
2136         }
2137     );
2138
2139     warnings_are{
2140        ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2141     } ["Checked","Checked"], "CanItemBeReserved only called once per available item if it returns a negative result for all items for a borrower";
2142     is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
2143
2144 };
2145
2146 {
2147     # Don't allow renewing onsite checkout
2148     my $branch   = $library->{branchcode};
2149
2150     #Create another record
2151     my $biblio = $builder->build_sample_biblio();
2152
2153     my $item = $builder->build_sample_item(
2154         {
2155             biblionumber     => $biblio->biblionumber,
2156             library          => $branch,
2157             itype            => $itemtype,
2158         }
2159     );
2160
2161     my $borrower = $builder->build_object(
2162         {
2163             class => 'Koha::Patrons',
2164             value => {
2165                 firstname =>  'fn',
2166                 surname => 'dn',
2167                 categorycode => $patron_category->{categorycode},
2168                 branchcode => $branch,
2169             }
2170         }
2171     );
2172
2173     my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
2174     my ( $renewed, $error ) = CanBookBeRenewed( $borrower, $issue );
2175     is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
2176     is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
2177 }
2178
2179 {
2180     my $library = $builder->build({ source => 'Branch' });
2181
2182     my $biblio = $builder->build_sample_biblio();
2183
2184     my $item = $builder->build_sample_item(
2185         {
2186             biblionumber     => $biblio->biblionumber,
2187             library          => $library->{branchcode},
2188             itype            => $itemtype,
2189         }
2190     );
2191     my $patron = $builder->build_object( { class => 'Koha::Patrons',  value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2192
2193     my $issue = AddIssue( $patron, $item->barcode );
2194     UpdateFine(
2195         {
2196             issue_id       => $issue->id,
2197             itemnumber     => $item->itemnumber,
2198             borrowernumber => $patron->borrowernumber,
2199             amount         => 1,
2200             type           => q{}
2201         }
2202     );
2203     UpdateFine(
2204         {
2205             issue_id       => $issue->id,
2206             itemnumber     => $item->itemnumber,
2207             borrowernumber => $patron->borrowernumber,
2208             amount         => 2,
2209             type           => q{}
2210         }
2211     );
2212     is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
2213 }
2214
2215 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
2216     plan tests => 24;
2217
2218     my $homebranch    = $builder->build( { source => 'Branch' } );
2219     my $holdingbranch = $builder->build( { source => 'Branch' } );
2220     my $otherbranch   = $builder->build( { source => 'Branch' } );
2221     my $patron_1      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2222     my $patron_2      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2223
2224     my $item = $builder->build_sample_item(
2225         {
2226             homebranch    => $homebranch->{branchcode},
2227             holdingbranch => $holdingbranch->{branchcode},
2228         }
2229     );
2230     Koha::CirculationRules->set_rules(
2231         {
2232             categorycode => undef,
2233             itemtype     => $item->effective_itemtype,
2234             branchcode   => undef,
2235             rules        => {
2236                 reservesallowed => 25,
2237                 issuelength     => 14,
2238                 lengthunit      => 'days',
2239                 renewalsallowed => 1,
2240                 renewalperiod   => 7,
2241                 norenewalbefore => undef,
2242                 auto_renew      => 0,
2243                 fine            => .10,
2244                 chargeperiod    => 1,
2245                 maxissueqty     => 20
2246             }
2247         }
2248     );
2249
2250     set_userenv($holdingbranch);
2251
2252     my $issue = AddIssue( $patron_1, $item->barcode );
2253     is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
2254
2255     my ( $error, $question, $alerts );
2256
2257     # AllowReturnToBranch == anywhere
2258     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2259     ## Test that unknown barcodes don't generate internal server errors
2260     set_userenv($homebranch);
2261     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
2262     ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
2263     ## Can be issued from homebranch
2264     set_userenv($homebranch);
2265     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2266     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2267     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2268     ## Can be issued from holdingbranch
2269     set_userenv($holdingbranch);
2270     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2271     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2272     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2273     ## Can be issued from another branch
2274     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2275     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2276     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2277
2278     # AllowReturnToBranch == holdingbranch
2279     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
2280     ## Cannot be issued from homebranch
2281     set_userenv($homebranch);
2282     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2283     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2284     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2285     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
2286     ## Can be issued from holdinbranch
2287     set_userenv($holdingbranch);
2288     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2289     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2290     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2291     ## Cannot be issued from another branch
2292     set_userenv($otherbranch);
2293     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2294     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2295     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2296     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
2297
2298     # AllowReturnToBranch == homebranch
2299     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
2300     ## Can be issued from holdinbranch
2301     set_userenv($homebranch);
2302     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2303     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2304     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2305     ## Cannot be issued from holdinbranch
2306     set_userenv($holdingbranch);
2307     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2308     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2309     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2310     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
2311     ## Cannot be issued from holdinbranch
2312     set_userenv($otherbranch);
2313     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2314     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2315     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2316     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
2317
2318     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
2319 };
2320
2321 subtest 'AddIssue & AllowReturnToBranch' => sub {
2322     plan tests => 9;
2323
2324     my $homebranch    = $builder->build( { source => 'Branch' } );
2325     my $holdingbranch = $builder->build( { source => 'Branch' } );
2326     my $otherbranch   = $builder->build( { source => 'Branch' } );
2327
2328     my $patron_1  = $builder->build_object({
2329         class => 'Koha::Patrons',
2330         value => { categorycode => $patron_category->{categorycode} }
2331     });
2332
2333     my $patron_2  = $builder->build_object({
2334         class => 'Koha::Patrons',
2335         value => { categorycode => $patron_category->{categorycode} }
2336     });
2337
2338
2339     my $item = $builder->build_sample_item(
2340         {
2341             homebranch    => $homebranch->{branchcode},
2342             holdingbranch => $holdingbranch->{branchcode},
2343         }
2344     );
2345
2346     set_userenv($holdingbranch);
2347
2348     my $ref_issue = 'Koha::Checkout';
2349     my $issue = AddIssue( $patron_1, $item->barcode );
2350
2351     my ( $error, $question, $alerts );
2352
2353     # AllowReturnToBranch == homebranch
2354     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2355     ## Can be issued from homebranch
2356     set_userenv($homebranch);
2357     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
2358     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2359     ## Can be issued from holdinbranch
2360     set_userenv($holdingbranch);
2361     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
2362     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2363     ## Can be issued from another branch
2364     set_userenv($otherbranch);
2365     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
2366     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2367
2368     # AllowReturnToBranch == holdinbranch
2369     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
2370     ## Cannot be issued from homebranch
2371     set_userenv($homebranch);
2372     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
2373     ## Can be issued from holdingbranch
2374     set_userenv($holdingbranch);
2375     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
2376     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2377     ## Cannot be issued from another branch
2378     set_userenv($otherbranch);
2379     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
2380
2381     # AllowReturnToBranch == homebranch
2382     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
2383     ## Can be issued from homebranch
2384     set_userenv($homebranch);
2385     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
2386     AddIssue( $patron_1, $item->barcode ); # Re-issue to patron 1
2387     ## Cannot be issued from holdinbranch
2388     set_userenv($holdingbranch);
2389     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
2390     ## Cannot be issued from another branch
2391     set_userenv($otherbranch);
2392     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
2393     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
2394 };
2395
2396 subtest 'AddIssue | recalls' => sub {
2397     plan tests => 3;
2398
2399     t::lib::Mocks::mock_preference("UseRecalls", 1);
2400     t::lib::Mocks::mock_preference("item-level_itypes", 1);
2401     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
2402     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
2403     my $item = $builder->build_sample_item;
2404     Koha::CirculationRules->set_rules({
2405         branchcode => undef,
2406         itemtype => undef,
2407         categorycode => undef,
2408         rules => {
2409             recalls_allowed => 10,
2410         },
2411     });
2412
2413     # checking out item that they have recalled
2414     my $recall1 = Koha::Recall->new(
2415         {   patron_id         => $patron1->borrowernumber,
2416             biblio_id         => $item->biblionumber,
2417             item_id           => $item->itemnumber,
2418             item_level        => 1,
2419             pickup_library_id => $patron1->branchcode,
2420         }
2421     )->store;
2422     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall1->id } );
2423     $recall1 = Koha::Recalls->find( $recall1->id );
2424     is( $recall1->fulfilled, 1, 'Recall was fulfilled when patron checked out item' );
2425     AddReturn( $item->barcode, $item->homebranch );
2426
2427     # this item is has a recall request. cancel recall
2428     my $recall2 = Koha::Recall->new(
2429         {   patron_id         => $patron2->borrowernumber,
2430             biblio_id         => $item->biblionumber,
2431             item_id           => $item->itemnumber,
2432             item_level        => 1,
2433             pickup_library_id => $patron2->branchcode,
2434         }
2435     )->store;
2436     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall2->id, cancel_recall => 'cancel' } );
2437     $recall2 = Koha::Recalls->find( $recall2->id );
2438     is( $recall2->cancelled, 1, 'Recall was cancelled when patron checked out item' );
2439     AddReturn( $item->barcode, $item->homebranch );
2440
2441     # this item is waiting to fulfill a recall. revert recall
2442     my $recall3 = Koha::Recall->new(
2443         {   patron_id         => $patron2->borrowernumber,
2444             biblio_id         => $item->biblionumber,
2445             item_id           => $item->itemnumber,
2446             item_level        => 1,
2447             pickup_library_id => $patron2->branchcode,
2448         }
2449     )->store;
2450     $recall3->set_waiting;
2451     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall3->id, cancel_recall => 'revert' } );
2452     $recall3 = Koha::Recalls->find( $recall3->id );
2453     is( $recall3->requested, 1, 'Recall was reverted from waiting when patron checked out item' );
2454     AddReturn( $item->barcode, $item->homebranch );
2455 };
2456
2457 subtest 'AddIssue & illrequests.due_date' => sub {
2458     plan tests => 2;
2459
2460     t::lib::Mocks::mock_preference( 'ILLModule', 1 );
2461     my $library = $builder->build( { source => 'Branch' } );
2462     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2463     my $item = $builder->build_sample_item();
2464
2465     set_userenv($library);
2466
2467     my $custom_date_due = '4999-12-18 12:34:56';
2468     my $expected_date_due = '4999-12-18 23:59:00';
2469     my $illrequest = Koha::ILL::Request->new({
2470         borrowernumber => $patron->borrowernumber,
2471         biblio_id => $item->biblionumber,
2472         branchcode => $library->{'branchcode'},
2473         due_date => $custom_date_due,
2474     })->store;
2475
2476     my $issue = AddIssue( $patron, $item->barcode );
2477     is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
2478
2479     $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2480     $item = $builder->build_sample_item();
2481     $custom_date_due = '4999-12-19';
2482     $expected_date_due = '4999-12-19 23:59:00';
2483     $illrequest = Koha::ILL::Request->new({
2484         borrowernumber => $patron->borrowernumber,
2485         biblio_id => $item->biblionumber,
2486         branchcode => $library->{'branchcode'},
2487         due_date => $custom_date_due,
2488     })->store;
2489
2490     $issue = AddIssue( $patron, $item->barcode );
2491     is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
2492 };
2493
2494 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
2495     plan tests => 8;
2496
2497     my $library = $builder->build( { source => 'Branch' } );
2498     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2499     my $item_1 = $builder->build_sample_item(
2500         {
2501             library => $library->{branchcode},
2502         }
2503     );
2504     my $item_2 = $builder->build_sample_item(
2505         {
2506             library => $library->{branchcode},
2507         }
2508     );
2509     Koha::CirculationRules->set_rules(
2510         {
2511             categorycode => undef,
2512             itemtype     => undef,
2513             branchcode   => $library->{branchcode},
2514             rules        => {
2515                 reservesallowed => 25,
2516                 issuelength     => 14,
2517                 lengthunit      => 'days',
2518                 renewalsallowed => 1,
2519                 renewalperiod   => 7,
2520                 norenewalbefore => undef,
2521                 auto_renew      => 0,
2522                 fine            => .10,
2523                 chargeperiod    => 1,
2524                 maxissueqty     => 20
2525             }
2526         }
2527     );
2528
2529     my ( $error, $question, $alerts );
2530
2531     # Patron cannot issue item_1, they have overdues
2532     my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
2533     my $issue = AddIssue( $patron, $item_1->barcode, $yesterday );    # Add an overdue
2534
2535     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
2536     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2537     is( keys(%$error) + keys(%$alerts),  0, 'No key for error and alert' . str($error, $question, $alerts) );
2538     is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
2539
2540     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
2541     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2542     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2543     is( $error->{USERBLOCKEDOVERDUE},      1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
2544
2545     # Patron cannot issue item_1, they are debarred
2546     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
2547     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
2548     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2549     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2550     is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
2551
2552     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
2553     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2554     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2555     is( $error->{USERBLOCKEDNOENDDATE},    '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
2556 };
2557
2558 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
2559     plan tests => 13;
2560
2561     my $library           = $builder->build_object( { class => 'Koha::Libraries' } );
2562     my $patron_category_x = $builder->build_object(
2563         {
2564             class => 'Koha::Patron::Categories',
2565             value => { category_type => 'X' }
2566         }
2567     );
2568     my $patron = $builder->build_object(
2569         {
2570             class => 'Koha::Patrons',
2571             value => {
2572                 categorycode  => $patron_category_x->categorycode,
2573                 gonenoaddress => undef,
2574                 lost          => undef,
2575                 debarred      => undef,
2576                 borrowernotes => ""
2577             }
2578         }
2579     );
2580     my $item_1 = $builder->build_sample_item(
2581         {
2582             library => $library->{branchcode},
2583         }
2584     );
2585
2586     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->barcode );
2587     is(
2588         $error->{STATS}, 1,
2589         '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)'
2590     );
2591
2592     my $stat = Koha::Statistics->search( { itemnumber => $item_1->itemnumber } )->next;
2593     is( $stat->branch,         C4::Context->userenv->{'branch'}, 'Recorded a branch' );
2594     is( $stat->type,           'localuse',                       'Recorded type as localuse' );
2595     is( $stat->itemnumber,     $item_1->itemnumber,              'Recorded an itemnumber' );
2596     is( $stat->itemtype,       $item_1->effective_itemtype,      'Recorded an itemtype' );
2597     is( $stat->borrowernumber, $patron->borrowernumber,          'Recorded a borrower number' );
2598     is( $stat->ccode,          $item_1->ccode,                   'Recorded a collection code' );
2599     is( $stat->categorycode,   $patron->categorycode,            'Recorded a categorycode' );
2600     is( $stat->location,       $item_1->location,                'Recorded a location' );
2601
2602     t::lib::Mocks::mock_userenv( { branchcode => $library->branchcode } );
2603     my $patron_2 = $builder->build_object(
2604         { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2605     my $item_2 = $builder->build_sample_item( { library => $library->branchcode } );
2606     my $issue  = AddIssue( $patron_2, $item_2->barcode );
2607     $item_2->discard_changes;
2608     ok( $item_2->onloan, "Item is checked out" );
2609
2610     my $item_3 = $builder->build_sample_item( { library => $library->branchcode } );
2611     CanBookBeIssued( $patron, $item_3->barcode );
2612     $item_3->discard_changes;
2613     is(
2614         Koha::Statistics->search( { itemnumber => $item_3->itemnumber } )->count, 1,
2615         'Single entry recorded in the stats table'
2616     );
2617
2618     my $item_4 = $builder->build_sample_item( { library => $library->branchcode } );
2619     AddIssue( $patron_2, $item_4->barcode );
2620     $item_4->discard_changes;
2621     is(
2622         Koha::Statistics->search( { itemnumber => $item_4->itemnumber } )->count, 1,
2623         'Issue should be recorded in statistics table for item 4.'
2624     );
2625     CanBookBeIssued( $patron, $item_4->barcode );
2626     $item_4->discard_changes;
2627     is(
2628         Koha::Statistics->search( { itemnumber => $item_4->itemnumber } )->count, 2,
2629         'Issue, return, and localuse should be recorded in statistics table for item 4.'
2630     );
2631
2632     # TODO There are other tests to provide here
2633 };
2634
2635 subtest 'MultipleReserves' => sub {
2636     plan tests => 3;
2637
2638     my $biblio = $builder->build_sample_biblio();
2639
2640     my $branch = $library2->{branchcode};
2641
2642     my $item_1 = $builder->build_sample_item(
2643         {
2644             biblionumber     => $biblio->biblionumber,
2645             library          => $branch,
2646             replacementprice => 12.00,
2647             itype            => $itemtype,
2648         }
2649     );
2650
2651     my $item_2 = $builder->build_sample_item(
2652         {
2653             biblionumber     => $biblio->biblionumber,
2654             library          => $branch,
2655             replacementprice => 12.00,
2656             itype            => $itemtype,
2657         }
2658     );
2659
2660     my $bibitems       = '';
2661     my $priority       = '1';
2662     my $resdate        = undef;
2663     my $expdate        = undef;
2664     my $notes          = '';
2665     my $checkitem      = undef;
2666     my $found          = undef;
2667
2668     # Renewing borrower
2669     my $patron = $builder->build_object(
2670         {
2671             class => 'Koha::Patrons',
2672             value => {
2673                 firstname =>  'John',
2674                 surname => 'Renewal',
2675                 categorycode => $patron_category->{categorycode},
2676                 branchcode => $branch,
2677             }
2678         }
2679     );
2680
2681     my $issue = AddIssue( $patron, $item_1->barcode);
2682     my $datedue = dt_from_string( $issue->date_due() );
2683     is (defined $issue->date_due(), 1, "item 1 checked out");
2684     my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
2685
2686     my $reserving_borrowernumber1 = $builder->build_object(
2687         {
2688             class => 'Koha::Patrons',
2689             value => {
2690                 firstname =>  'Katrin',
2691                 surname => 'Reservation',
2692                 categorycode => $patron_category->{categorycode},
2693                 branchcode => $branch,
2694             }
2695         }
2696     )->borrowernumber;
2697
2698     AddReserve(
2699         {
2700             branchcode       => $branch,
2701             borrowernumber   => $reserving_borrowernumber1,
2702             biblionumber     => $biblio->biblionumber,
2703             priority         => $priority,
2704             reservation_date => $resdate,
2705             expiration_date  => $expdate,
2706             notes            => $notes,
2707             itemnumber       => $checkitem,
2708             found            => $found,
2709         }
2710     );
2711
2712     my $reserving_borrowernumber2 = $builder->build_object(
2713         {
2714             class => 'Koha::Patrons',
2715             value => {
2716                 firstname =>  'Kirk',
2717                 surname => 'Reservation',
2718                 categorycode => $patron_category->{categorycode},
2719                 branchcode => $branch,
2720             }
2721         }
2722     )->borrowernumber;
2723
2724     AddReserve(
2725         {
2726             branchcode       => $branch,
2727             borrowernumber   => $reserving_borrowernumber2,
2728             biblionumber     => $biblio->biblionumber,
2729             priority         => $priority,
2730             reservation_date => $resdate,
2731             expiration_date  => $expdate,
2732             notes            => $notes,
2733             itemnumber       => $checkitem,
2734             found            => $found,
2735         }
2736     );
2737
2738     {
2739         my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue, 1);
2740         is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
2741     }
2742
2743     my $item_3 = $builder->build_sample_item(
2744         {
2745             biblionumber     => $biblio->biblionumber,
2746             library          => $branch,
2747             replacementprice => 12.00,
2748             itype            => $itemtype,
2749         }
2750     );
2751
2752     {
2753         my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue, 1);
2754         is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
2755     }
2756 };
2757
2758 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
2759     plan tests => 5;
2760
2761     my $library = $builder->build( { source => 'Branch' } );
2762     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2763
2764     my $biblionumber = $builder->build_sample_biblio(
2765         {
2766             branchcode => $library->{branchcode},
2767         }
2768     )->biblionumber;
2769     my $item_1 = $builder->build_sample_item(
2770         {
2771             biblionumber => $biblionumber,
2772             library      => $library->{branchcode},
2773         }
2774     );
2775
2776     my $item_2 = $builder->build_sample_item(
2777         {
2778             biblionumber => $biblionumber,
2779             library      => $library->{branchcode},
2780         }
2781     );
2782
2783     Koha::CirculationRules->set_rules(
2784         {
2785             categorycode => undef,
2786             itemtype     => undef,
2787             branchcode   => $library->{branchcode},
2788             rules        => {
2789                 reservesallowed => 25,
2790                 issuelength     => 14,
2791                 lengthunit      => 'days',
2792                 renewalsallowed => 1,
2793                 renewalperiod   => 7,
2794                 norenewalbefore => undef,
2795                 auto_renew      => 0,
2796                 fine            => .10,
2797                 chargeperiod    => 1,
2798                 maxissueqty     => 20
2799             }
2800         }
2801     );
2802
2803     my ( $error, $question, $alerts );
2804     my $issue = AddIssue( $patron, $item_1->barcode, dt_from_string->add( days => 1 ) );
2805
2806     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2807     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2808     cmp_deeply(
2809         { error => $error, alerts => $alerts },
2810         { error => {}, alerts => {} },
2811         'No error or alert should be raised'
2812     );
2813     is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
2814
2815     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2816     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2817     cmp_deeply(
2818         { error => $error, question => $question, alerts => $alerts },
2819         { error => {}, question => {}, alerts => {} },
2820         'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
2821     );
2822
2823     # Add a subscription
2824     Koha::Subscription->new({ biblionumber => $biblionumber })->store;
2825
2826     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2827     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2828     cmp_deeply(
2829         { error => $error, question => $question, alerts => $alerts },
2830         { error => {}, question => {}, alerts => {} },
2831         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2832     );
2833
2834     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2835     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2836     cmp_deeply(
2837         { error => $error, question => $question, alerts => $alerts },
2838         { error => {}, question => {}, alerts => {} },
2839         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2840     );
2841 };
2842
2843 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
2844     plan tests => 8;
2845
2846     my $library = $builder->build( { source => 'Branch' } );
2847     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2848
2849     # Add 2 items
2850     my $biblionumber = $builder->build_sample_biblio(
2851         {
2852             branchcode => $library->{branchcode},
2853         }
2854     )->biblionumber;
2855     my $item_1 = $builder->build_sample_item(
2856         {
2857             biblionumber => $biblionumber,
2858             library      => $library->{branchcode},
2859         }
2860     );
2861     my $item_2 = $builder->build_sample_item(
2862         {
2863             biblionumber => $biblionumber,
2864             library      => $library->{branchcode},
2865         }
2866     );
2867
2868     # And the circulation rule
2869     Koha::CirculationRules->search->delete;
2870     Koha::CirculationRules->set_rules(
2871         {
2872             categorycode => undef,
2873             itemtype     => undef,
2874             branchcode   => undef,
2875             rules        => {
2876                 issuelength => 1,
2877                 firstremind => 1,        # 1 day of grace
2878                 finedays    => 2,        # 2 days of fine per day of overdue
2879                 lengthunit  => 'days',
2880             }
2881         }
2882     );
2883
2884     # Patron cannot issue item_1, they have overdues
2885     my $now = dt_from_string;
2886     my $five_days_ago = $now->clone->subtract( days => 5 );
2887     my $ten_days_ago  = $now->clone->subtract( days => 10 );
2888     AddIssue( $patron, $item_1->barcode, $five_days_ago );    # Add an overdue
2889     AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2890       ;    # Add another overdue
2891
2892     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2893     AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2894     my $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2895     is( $suspensions->count, 1, "Suspension added" );
2896     my $THE_suspension = $suspensions->next;
2897
2898     # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2899     # Same for the others
2900     my $expected_expiration = output_pref(
2901         {
2902             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2903             dateformat => 'sql',
2904             dateonly   => 1
2905         }
2906     );
2907     is( $THE_suspension->expiration, $expected_expiration, "Suspesion expiration set" );
2908
2909     AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2910     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2911     is( $suspensions->count, 1, "Only one suspension" );
2912     $THE_suspension = $suspensions->next;
2913
2914     $expected_expiration = output_pref(
2915         {
2916             dt         => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2917             dateformat => 'sql',
2918             dateonly   => 1
2919         }
2920     );
2921     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
2922
2923     Koha::Patron::Debarments::DelUniqueDebarment(
2924         { borrowernumber => $patron->borrowernumber, type => 'SUSPENSION' } );
2925
2926     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2927     AddIssue( $patron, $item_1->barcode, $five_days_ago );    # Add an overdue
2928     AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2929       ;    # Add another overdue
2930     AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2931     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2932     is( $suspensions->count, 1, "Only one suspension" );
2933     $THE_suspension = $suspensions->next;
2934
2935     $expected_expiration = output_pref(
2936         {
2937             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2938             dateformat => 'sql',
2939             dateonly   => 1
2940         }
2941     );
2942     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
2943
2944     AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2945     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2946     is( $suspensions->count, 1, "Only one suspension" );
2947     $THE_suspension = $suspensions->next;
2948
2949     $expected_expiration = output_pref(
2950         {
2951             dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
2952             dateformat => 'sql',
2953             dateonly   => 1
2954         }
2955     );
2956     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
2957 };
2958
2959 subtest 'AddReturn + suspension_chargeperiod' => sub {
2960     plan tests => 29;
2961
2962     my $library = $builder->build( { source => 'Branch' } );
2963     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2964
2965     my $biblionumber = $builder->build_sample_biblio(
2966         {
2967             branchcode => $library->{branchcode},
2968         }
2969     )->biblionumber;
2970     my $item_1 = $builder->build_sample_item(
2971         {
2972             biblionumber => $biblionumber,
2973             library      => $library->{branchcode},
2974         }
2975     );
2976
2977     # And the issuing rule
2978     Koha::CirculationRules->search->delete;
2979     Koha::CirculationRules->set_rules(
2980         {
2981             categorycode => '*',
2982             itemtype     => '*',
2983             branchcode   => '*',
2984             rules        => {
2985                 issuelength => 1,
2986                 firstremind => 0,    # 0 day of grace
2987                 finedays    => 2,    # 2 days of fine per day of overdue
2988                 suspension_chargeperiod => 1,
2989                 lengthunit              => 'days',
2990             }
2991         }
2992     );
2993
2994     my $now = dt_from_string;
2995     my $five_days_ago = $now->clone->subtract( days => 5 );
2996     # We want to charge 2 days every day, without grace
2997     # With 5 days of overdue: 5 * Z
2998     my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2999     test_debarment_on_checkout(
3000         {
3001             item            => $item_1,
3002             library         => $library,
3003             patron          => $patron,
3004             due_date        => $five_days_ago,
3005             expiration_date => $expected_expiration,
3006         }
3007     );
3008
3009     # Same with undef firstremind
3010     Koha::CirculationRules->search->delete;
3011     Koha::CirculationRules->set_rules(
3012         {
3013             categorycode => '*',
3014             itemtype     => '*',
3015             branchcode   => '*',
3016             rules        => {
3017                 issuelength => 1,
3018                 firstremind => undef,    # 0 day of grace
3019                 finedays    => 2,    # 2 days of fine per day of overdue
3020                 suspension_chargeperiod => 1,
3021                 lengthunit              => 'days',
3022             }
3023         }
3024     );
3025     {
3026     my $now = dt_from_string;
3027     my $five_days_ago = $now->clone->subtract( days => 5 );
3028     # We want to charge 2 days every day, without grace
3029     # With 5 days of overdue: 5 * Z
3030     my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
3031     test_debarment_on_checkout(
3032         {
3033             item            => $item_1,
3034             library         => $library,
3035             patron          => $patron,
3036             due_date        => $five_days_ago,
3037             expiration_date => $expected_expiration,
3038         }
3039     );
3040     }
3041     # We want to charge 2 days every 2 days, without grace
3042     # With 5 days of overdue: (5 * 2) / 2
3043     Koha::CirculationRules->set_rule(
3044         {
3045             categorycode => undef,
3046             branchcode   => undef,
3047             itemtype     => undef,
3048             rule_name    => 'suspension_chargeperiod',
3049             rule_value   => '2',
3050         }
3051     );
3052
3053     $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
3054     test_debarment_on_checkout(
3055         {
3056             item            => $item_1,
3057             library         => $library,
3058             patron          => $patron,
3059             due_date        => $five_days_ago,
3060             expiration_date => $expected_expiration,
3061         }
3062     );
3063
3064     # We want to charge 2 days every 3 days, with 1 day of grace
3065     # With 5 days of overdue: ((5-1) / 3 ) * 2
3066     Koha::CirculationRules->set_rules(
3067         {
3068             categorycode => undef,
3069             branchcode   => undef,
3070             itemtype     => undef,
3071             rules        => {
3072                 suspension_chargeperiod => 3,
3073                 firstremind             => 1,
3074             }
3075         }
3076     );
3077     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
3078     test_debarment_on_checkout(
3079         {
3080             item            => $item_1,
3081             library         => $library,
3082             patron          => $patron,
3083             due_date        => $five_days_ago,
3084             expiration_date => $expected_expiration,
3085         }
3086     );
3087
3088     # Use finesCalendar to know if holiday must be skipped to calculate the due date
3089     # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
3090     Koha::CirculationRules->set_rules(
3091         {
3092             categorycode => undef,
3093             branchcode   => undef,
3094             itemtype     => undef,
3095             rules        => {
3096                 finedays                => 2,
3097                 suspension_chargeperiod => 1,
3098                 firstremind             => 0,
3099             }
3100         }
3101     );
3102     t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
3103     t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
3104
3105     # Adding a holiday 2 days ago
3106     my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
3107     my $two_days_ago = $now->clone->subtract( days => 2 );
3108     $calendar->insert_single_holiday(
3109         day             => $two_days_ago->day,
3110         month           => $two_days_ago->month,
3111         year            => $two_days_ago->year,
3112         title           => 'holidayTest-2d',
3113         description     => 'holidayDesc 2 days ago'
3114     );
3115     # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
3116     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
3117     test_debarment_on_checkout(
3118         {
3119             item            => $item_1,
3120             library         => $library,
3121             patron          => $patron,
3122             due_date        => $five_days_ago,
3123             expiration_date => $expected_expiration,
3124         }
3125     );
3126
3127     # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
3128     my $two_days_ahead = $now->clone->add( days => 2 );
3129     $calendar->insert_single_holiday(
3130         day             => $two_days_ahead->day,
3131         month           => $two_days_ahead->month,
3132         year            => $two_days_ahead->year,
3133         title           => 'holidayTest+2d',
3134         description     => 'holidayDesc 2 days ahead'
3135     );
3136
3137     # Same as above, but we should skip D+2
3138     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
3139     test_debarment_on_checkout(
3140         {
3141             item            => $item_1,
3142             library         => $library,
3143             patron          => $patron,
3144             due_date        => $five_days_ago,
3145             expiration_date => $expected_expiration,
3146         }
3147     );
3148
3149     # Adding another holiday, day of expiration date
3150     my $expected_expiration_dt = dt_from_string($expected_expiration);
3151     $calendar->insert_single_holiday(
3152         day             => $expected_expiration_dt->day,
3153         month           => $expected_expiration_dt->month,
3154         year            => $expected_expiration_dt->year,
3155         title           => 'holidayTest_exp',
3156         description     => 'holidayDesc on expiration date'
3157     );
3158     # Expiration date will be the day after
3159     test_debarment_on_checkout(
3160         {
3161             item            => $item_1,
3162             library         => $library,
3163             patron          => $patron,
3164             due_date        => $five_days_ago,
3165             expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
3166         }
3167     );
3168
3169     test_debarment_on_checkout(
3170         {
3171             item            => $item_1,
3172             library         => $library,
3173             patron          => $patron,
3174             return_date     => $now->clone->add(days => 5),
3175             expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
3176         }
3177     );
3178
3179     test_debarment_on_checkout(
3180         {
3181             item            => $item_1,
3182             library         => $library,
3183             patron          => $patron,
3184             due_date        => $now->clone->add(days => 1),
3185             return_date     => $now->clone->add(days => 5),
3186             expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
3187         }
3188     );
3189
3190     Koha::CirculationRules->search->delete;
3191     Koha::CirculationRules->set_rules(
3192         {
3193             categorycode => undef,
3194             itemtype     => undef,
3195             branchcode   => undef,
3196             rules => {
3197                 finedays   => 0,
3198                 lengthunit => 'days',
3199               }
3200         }
3201     );
3202
3203     Koha::Patron::Debarments::AddDebarment(
3204         {
3205             borrowernumber => $patron->borrowernumber,
3206             expiration     => '9999-12-31',
3207             type           => 'MANUAL',
3208         }
3209     );
3210
3211     AddIssue( $patron, $item_1->barcode, $now->clone->subtract( days => 1 ) );
3212     my ( undef, $message ) = AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
3213     is( $message->{WasReturned} && exists $message->{ForeverDebarred}, 1, 'Forever debarred message for Addreturn when overdue');
3214
3215     Koha::Patron::Debarments::DelUniqueDebarment(
3216         {
3217             borrowernumber => $patron->borrowernumber,
3218             type           => 'MANUAL',
3219         }
3220     );
3221     Koha::Patron::Debarments::AddDebarment(
3222         {
3223             borrowernumber => $patron->borrowernumber,
3224             expiration     => $now->clone->add( days => 10 ),
3225             type           => 'MANUAL',
3226         }
3227     );
3228
3229     AddIssue( $patron, $item_1->barcode, $now->clone->subtract( days => 1 ) );
3230     (undef, $message) = AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
3231     is( $message->{WasReturned} && exists $message->{PrevDebarred}, 1, 'Previously debarred message for Addreturn when overdue');
3232 };
3233
3234 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
3235     plan tests => 2;
3236
3237     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
3238     my $patron1 = $builder->build_object(
3239         {
3240             class => 'Koha::Patrons',
3241             value => {
3242                 branchcode   => $library->branchcode,
3243                 categorycode => $patron_category->{categorycode}
3244             }
3245         }
3246     );
3247     my $patron2 = $builder->build_object(
3248         {
3249             class => 'Koha::Patrons',
3250             value => {
3251                 branchcode   => $library->branchcode,
3252                 categorycode => $patron_category->{categorycode}
3253             }
3254         }
3255     );
3256
3257     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
3258
3259     my $item = $builder->build_sample_item(
3260         {
3261             library      => $library->branchcode,
3262         }
3263     );
3264
3265     my ( $error, $question, $alerts );
3266     my $issue = AddIssue( $patron1, $item->barcode );
3267
3268     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
3269     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
3270     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' );
3271
3272     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
3273     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
3274     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' );
3275
3276     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
3277 };
3278
3279
3280 subtest 'AddReturn | is_overdue' => sub {
3281     plan tests => 9;
3282
3283     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
3284     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
3285     t::lib::Mocks::mock_preference('finesMode', 'production');
3286     t::lib::Mocks::mock_preference('MaxFine', '100');
3287
3288     my $library = $builder->build( { source => 'Branch' } );
3289     my $patron  = $builder->build_object(
3290         {
3291             class => 'Koha::Patrons',
3292             value => { categorycode => $patron_category->{categorycode} }
3293         }
3294     );
3295     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
3296     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3297
3298     my $item = $builder->build_sample_item(
3299         {
3300             library      => $library->{branchcode},
3301             replacementprice => 7
3302         }
3303     );
3304
3305     Koha::CirculationRules->search->delete;
3306     Koha::CirculationRules->set_rules(
3307         {
3308             categorycode => undef,
3309             itemtype     => undef,
3310             branchcode   => undef,
3311             rules        => {
3312                 issuelength  => 6,
3313                 lengthunit   => 'days',
3314                 fine         => 1,        # Charge 1 every day of overdue
3315                 chargeperiod => 1,
3316             }
3317         }
3318     );
3319
3320     my $now   = dt_from_string;
3321     my $one_day_ago   = $now->clone->subtract( days => 1 );
3322     my $two_days_ago  = $now->clone->subtract( days => 2 );
3323     my $five_days_ago = $now->clone->subtract( days => 5 );
3324     my $ten_days_ago  = $now->clone->subtract( days => 10 );
3325
3326     # No return date specified, today will be used => 10 days overdue charged
3327     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3328     AddReturn( $item->barcode, $library->{branchcode} );
3329     is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
3330     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3331
3332     # specify return date 5 days before => no overdue charged
3333     AddIssue( $patron, $item->barcode, $five_days_ago ); # date due was 5d ago
3334     AddReturn( $item->barcode, $library->{branchcode}, undef, $ten_days_ago );
3335     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
3336     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3337
3338     # specify return date 5 days later => 5 days overdue charged
3339     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3340     AddReturn( $item->barcode, $library->{branchcode}, undef, $five_days_ago );
3341     is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
3342     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3343
3344     # specify return date 5 days later, specify exemptfine => no overdue charge
3345     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3346     AddReturn( $item->barcode, $library->{branchcode}, 1, $five_days_ago );
3347     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
3348     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3349
3350     subtest 'bug 22877 | Lost item return' => sub {
3351
3352         plan tests => 3;
3353
3354         my $issue = AddIssue( $patron, $item->barcode, $ten_days_ago );    # date due was 10d ago
3355
3356         # Fake fines cronjob on this checkout
3357         my ($fine) =
3358           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3359             $ten_days_ago, $now );
3360         UpdateFine(
3361             {
3362                 issue_id       => $issue->issue_id,
3363                 itemnumber     => $item->itemnumber,
3364                 borrowernumber => $patron->borrowernumber,
3365                 amount         => $fine,
3366                 due            => output_pref($ten_days_ago)
3367             }
3368         );
3369         is( int( $patron->account->balance() ),
3370             10, "Overdue fine of 10 days overdue" );
3371
3372         # Fake longoverdue with charge and not marking returned
3373         LostItem( $item->itemnumber, 'cronjob', 0 );
3374         is( int( $patron->account->balance() ),
3375             17, "Lost fine of 7 plus 10 days overdue" );
3376
3377         # Now we return it today
3378         AddReturn( $item->barcode, $library->{branchcode} );
3379         is( int( $patron->account->balance() ),
3380             17, "Should have a single 10 days overdue fine and lost charge" );
3381
3382         # Cleanup
3383         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3384     };
3385
3386     subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
3387
3388         plan tests => 17;
3389
3390         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
3391
3392         my $issue = AddIssue( $patron, $item->barcode, $one_day_ago );    # date due was 1d ago
3393
3394         # Fake fines cronjob on this checkout
3395         my ($fine) =
3396           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3397             $one_day_ago, $now );
3398         UpdateFine(
3399             {
3400                 issue_id       => $issue->issue_id,
3401                 itemnumber     => $item->itemnumber,
3402                 borrowernumber => $patron->borrowernumber,
3403                 amount         => $fine,
3404                 due            => output_pref($one_day_ago)
3405             }
3406         );
3407         is( int( $patron->account->balance() ),
3408             1, "Overdue fine of 1 day overdue" );
3409
3410         # Backdated return (dropbox mode example - charge should be removed)
3411         AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
3412         is( int( $patron->account->balance() ),
3413             0, "Overdue fine should be annulled" );
3414         my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3415         is( $lines->count, 0, "Overdue fine accountline has been removed");
3416
3417         $issue = AddIssue( $patron, $item->barcode, $two_days_ago );    # date due was 2d ago
3418
3419         # Fake fines cronjob on this checkout
3420         ($fine) =
3421           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3422             $two_days_ago, $now );
3423         UpdateFine(
3424             {
3425                 issue_id       => $issue->issue_id,
3426                 itemnumber     => $item->itemnumber,
3427                 borrowernumber => $patron->borrowernumber,
3428                 amount         => $fine,
3429                 due            => output_pref($one_day_ago)
3430             }
3431         );
3432         is( int( $patron->account->balance() ),
3433             2, "Overdue fine of 2 days overdue" );
3434
3435         # Payment made against fine
3436         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3437         my $debit = $lines->next;
3438         my $credit = $patron->account->add_credit(
3439             {
3440                 amount    => 2,
3441                 type      => 'PAYMENT',
3442                 interface => 'test',
3443             }
3444         );
3445         $credit->apply( { debits => [$debit] } );
3446
3447         is( int( $patron->account->balance() ),
3448             0, "Overdue fine should be paid off" );
3449         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3450         is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
3451         my $line = $lines->next;
3452         is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
3453         is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
3454
3455         # Backdated return (dropbox mode example - charge should be removed)
3456         AddReturn( $item->barcode, $library->{branchcode}, undef, $one_day_ago );
3457         is( int( $patron->account->balance() ),
3458             -1, "Refund credit has been applied" );
3459         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
3460         is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
3461
3462         $line = $lines->next;
3463         is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
3464         is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
3465         is($line->status,'RETURNED', "Overdue fine is fixed");
3466         $line = $lines->next;
3467         is($line->amount+0,-2, "Original payment amount remains as 2");
3468         is($line->amountoutstanding+0,0, "Original payment remains applied");
3469         $line = $lines->next;
3470         is($line->amount+0,-1, "Refund amount correctly set to 1");
3471         is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
3472
3473         # Cleanup
3474         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3475     };
3476
3477     subtest 'bug 25417 | backdated return + exemptfine' => sub {
3478
3479         plan tests => 2;
3480
3481         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
3482
3483         my $issue = AddIssue( $patron, $item->barcode, $one_day_ago );    # date due was 1d ago
3484
3485         # Fake fines cronjob on this checkout
3486         my ($fine) =
3487           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3488             $one_day_ago, $now );
3489         UpdateFine(
3490             {
3491                 issue_id       => $issue->issue_id,
3492                 itemnumber     => $item->itemnumber,
3493                 borrowernumber => $patron->borrowernumber,
3494                 amount         => $fine,
3495                 due            => output_pref($one_day_ago)
3496             }
3497         );
3498         is( int( $patron->account->balance() ),
3499             1, "Overdue fine of 1 day overdue" );
3500
3501         # Backdated return (dropbox mode example - charge should no longer exist)
3502         AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
3503         is( int( $patron->account->balance() ),
3504             0, "Overdue fine should be annulled" );
3505
3506         # Cleanup
3507         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3508     };
3509
3510     subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
3511         plan tests => 7;
3512
3513         t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
3514
3515         my $due_date = dt_from_string;
3516         my $issue = AddIssue( $patron, $item->barcode, $due_date );
3517
3518         # Add fine
3519         UpdateFine(
3520             {
3521                 issue_id       => $issue->issue_id,
3522                 itemnumber     => $item->itemnumber,
3523                 borrowernumber => $patron->borrowernumber,
3524                 amount         => 0.25,
3525                 due            => output_pref($due_date)
3526             }
3527         );
3528         is( $patron->account->balance(),
3529             0.25, 'Overdue fine of $0.25 recorded' );
3530
3531         # Backdate return to exact due date and time
3532         my ( undef, $message ) =
3533           AddReturn( $item->barcode, $library->{branchcode},
3534             undef, $due_date );
3535
3536         my $accountline =
3537           Koha::Account::Lines->find( { issue_id => $issue->id } );
3538         ok( !$accountline, 'accountline removed as expected' );
3539
3540         # Re-issue
3541         $issue = AddIssue( $patron, $item->barcode, $due_date );
3542
3543         # Add fine
3544         UpdateFine(
3545             {
3546                 issue_id       => $issue->issue_id,
3547                 itemnumber     => $item->itemnumber,
3548                 borrowernumber => $patron->borrowernumber,
3549                 amount         => .25,
3550                 due            => output_pref($due_date)
3551             }
3552         );
3553         is( $patron->account->balance(),
3554             0.25, 'Overdue fine of $0.25 recorded' );
3555
3556         # Partial pay accruing fine
3557         my $lines = Koha::Account::Lines->search(
3558             {
3559                 borrowernumber => $patron->borrowernumber,
3560                 issue_id       => $issue->id
3561             }
3562         );
3563         my $debit  = $lines->next;
3564         my $credit = $patron->account->add_credit(
3565             {
3566                 amount    => .20,
3567                 type      => 'PAYMENT',
3568                 interface => 'test',
3569             }
3570         );
3571         $credit->apply( { debits => [$debit] } );
3572
3573         is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
3574
3575         # Backdate return to exact due date and time
3576         ( undef, $message ) =
3577           AddReturn( $item->barcode, $library->{branchcode},
3578             undef, $due_date );
3579
3580         $lines = Koha::Account::Lines->search(
3581             {
3582                 borrowernumber => $patron->borrowernumber,
3583                 issue_id       => $issue->id
3584             }
3585         );
3586         $accountline = $lines->next;
3587         is( $accountline->amountoutstanding + 0,
3588             0, 'Partially paid fee amount outstanding was reduced to 0' );
3589         is( $accountline->amount + 0,
3590             0, 'Partially paid fee amount was reduced to 0' );
3591         is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
3592
3593         # Cleanup
3594         Koha::Account::Lines->search(
3595             { borrowernumber => $patron->borrowernumber } )->delete;
3596     };
3597
3598     subtest 'enh 23091 | Lost item return policies' => sub {
3599         plan tests => 5;
3600
3601         my $manager = $builder->build_object({ class => "Koha::Patrons" });
3602
3603         my $branchcode_false =
3604           $builder->build( { source => 'Branch' } )->{branchcode};
3605         my $specific_rule_false = $builder->build(
3606             {
3607                 source => 'CirculationRule',
3608                 value  => {
3609                     branchcode   => $branchcode_false,
3610                     categorycode => undef,
3611                     itemtype     => undef,
3612                     rule_name    => 'lostreturn',
3613                     rule_value   => 0
3614                 }
3615             }
3616         );
3617         my $branchcode_refund =
3618           $builder->build( { source => 'Branch' } )->{branchcode};
3619         my $specific_rule_refund = $builder->build(
3620             {
3621                 source => 'CirculationRule',
3622                 value  => {
3623                     branchcode   => $branchcode_refund,
3624                     categorycode => undef,
3625                     itemtype     => undef,
3626                     rule_name    => 'lostreturn',
3627                     rule_value   => 'refund'
3628                 }
3629             }
3630         );
3631         my $branchcode_restore =
3632           $builder->build( { source => 'Branch' } )->{branchcode};
3633         my $specific_rule_restore = $builder->build(
3634             {
3635                 source => 'CirculationRule',
3636                 value  => {
3637                     branchcode   => $branchcode_restore,
3638                     categorycode => undef,
3639                     itemtype     => undef,
3640                     rule_name    => 'lostreturn',
3641                     rule_value   => 'restore'
3642                 }
3643             }
3644         );
3645         my $branchcode_charge =
3646           $builder->build( { source => 'Branch' } )->{branchcode};
3647         my $specific_rule_charge = $builder->build(
3648             {
3649                 source => 'CirculationRule',
3650                 value  => {
3651                     branchcode   => $branchcode_charge,
3652                     categorycode => undef,
3653                     itemtype     => undef,
3654                     rule_name    => 'lostreturn',
3655                     rule_value   => 'charge'
3656                 }
3657             }
3658         );
3659
3660         my $branchcode_refund_unpaid =
3661         $builder->build( { source => 'Branch' } )->{branchcode};
3662         my $specific_rule_refund_unpaid = $builder->build(
3663             {
3664                 source => 'CirculationRule',
3665                 value  => {
3666                     branchcode   => $branchcode_refund_unpaid,
3667                     categorycode => undef,
3668                     itemtype     => undef,
3669                     rule_name    => 'lostreturn',
3670                     rule_value   => 'refund_unpaid'
3671                 }
3672             }
3673         );
3674
3675         my $replacement_amount = 99.00;
3676         t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
3677         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
3678         t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
3679         t::lib::Mocks::mock_preference( 'BlockReturnOfLostItems',       0 );
3680         t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl',
3681             'CheckinLibrary' );
3682         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge',
3683             undef );
3684
3685         subtest 'lostreturn | refund_unpaid' => sub {
3686             plan tests => 21;
3687
3688             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund_unpaid });
3689
3690             my $item = $builder->build_sample_item(
3691                 {
3692                     replacementprice => $replacement_amount
3693                 }
3694             );
3695
3696             # Issue the item
3697             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3698
3699             # Mark item as lost
3700             $item->itemlost(3)->store;
3701             C4::Circulation::LostItem( $item->itemnumber, 1 );
3702
3703             my $lost_fee_lines = Koha::Account::Lines->search(
3704                 {
3705                     borrowernumber  => $patron->id,
3706                     itemnumber      => $item->itemnumber,
3707                     debit_type_code => 'LOST'
3708                 }
3709             );
3710             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3711             my $lost_fee_line = $lost_fee_lines->next;
3712             is( int($lost_fee_line->amount),
3713                 $replacement_amount, 'The right LOST amount is generated' );
3714             is( int($lost_fee_line->amountoutstanding),
3715                 $replacement_amount,
3716                 'The right LOST amountoutstanding is generated' );
3717             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3718
3719             is(
3720                 int($patron->account->balance),
3721                 $replacement_amount ,
3722                 "Account balance equals the replacement amount after being charged lost fee when no payments has been made"
3723             );
3724
3725             # Return lost item without any payments having been made
3726             my ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3727
3728             $lost_fee_line->discard_changes;
3729
3730             is( int($lost_fee_line->amount), $replacement_amount, 'The LOST amount is left intact' );
3731             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero' );
3732             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set' );
3733             is(
3734                 int($patron->account->balance),
3735                 0,
3736                 'Account balance should be zero after returning item with lost fee when no payments has been made'
3737             );
3738
3739             # Create a second item
3740             $item = $builder->build_sample_item(
3741                 {
3742                     replacementprice => $replacement_amount
3743                 }
3744             );
3745
3746             # Issue the item
3747             $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3748
3749             # Mark item as lost
3750             $item->itemlost(3)->store;
3751             C4::Circulation::LostItem( $item->itemnumber, 1 );
3752
3753             $lost_fee_lines = Koha::Account::Lines->search(
3754                 {
3755                     borrowernumber  => $patron->id,
3756                     itemnumber      => $item->itemnumber,
3757                     debit_type_code => 'LOST'
3758                 }
3759             );
3760             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3761             $lost_fee_line = $lost_fee_lines->next;
3762
3763             # Make partial payment
3764             $patron->account->payin_amount({
3765                 type => 'PAYMENT',
3766                 interface => 'intranet',
3767                 payment_type => 'CASH',
3768                 user_id => $patron->borrowernumber,
3769                 amount => 39.00,
3770                 debits => [$lost_fee_line]
3771             });
3772
3773             $lost_fee_line->discard_changes;
3774
3775             is( int($lost_fee_line->amountoutstanding),
3776                 60,
3777                 'The LOST amountoutstanding is the expected amount after partial payment of lost fee'
3778             );
3779
3780             is(
3781                 int($patron->account->balance),
3782                 60,
3783                 'Account balance is the expected amount after partial payment of lost fee'
3784             );
3785
3786              # Return lost item with partial payment having been made
3787             ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3788
3789             $lost_fee_line->discard_changes;
3790
3791             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero after returning lost item with partial payment' );
3792             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set for lost item with partial payment' );
3793             is(
3794                 int($patron->account->balance),
3795                 0,
3796                 'Account balance should be zero after returning item with lost fee when partial payment has been made'
3797             );
3798
3799             # Create a third item
3800             $item = $builder->build_sample_item(
3801                 {
3802                     replacementprice => $replacement_amount
3803                 }
3804             );
3805
3806             # Issue the item
3807             $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3808
3809             # Mark item as lost
3810             $item->itemlost(3)->store;
3811             C4::Circulation::LostItem( $item->itemnumber, 1 );
3812
3813             $lost_fee_lines = Koha::Account::Lines->search(
3814                 {
3815                     borrowernumber  => $patron->id,
3816                     itemnumber      => $item->itemnumber,
3817                     debit_type_code => 'LOST'
3818                 }
3819             );
3820             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3821             $lost_fee_line = $lost_fee_lines->next;
3822
3823             # Make full payment
3824             $patron->account->payin_amount({
3825                 type => 'PAYMENT',
3826                 interface => 'intranet',
3827                 payment_type => 'CASH',
3828                 user_id => $patron->borrowernumber,
3829                 amount => $replacement_amount,
3830                 debits => [$lost_fee_line]
3831             });
3832
3833             $lost_fee_line->discard_changes;
3834
3835             is( int($lost_fee_line->amountoutstanding),
3836                 0,
3837                 'The LOST amountoutstanding is the expected amount after partial payment of lost fee'
3838             );
3839
3840             is(
3841                 int($patron->account->balance),
3842                 0,
3843                 'Account balance is the expected amount after partial payment of lost fee'
3844             );
3845
3846              # Return lost item with partial payment having been made
3847             ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3848
3849             $lost_fee_line->discard_changes;
3850
3851             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero after returning lost item with full payment' );
3852             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set for lost item with partial payment' );
3853             is(
3854                 int($patron->account->balance),
3855                 0,
3856                 'Account balance should be zero after returning item with lost fee when full payment has been made'
3857             );
3858         };
3859
3860         subtest 'lostreturn | false' => sub {
3861             plan tests => 12;
3862
3863             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_false });
3864
3865             my $item = $builder->build_sample_item(
3866                 {
3867                     replacementprice => $replacement_amount
3868                 }
3869             );
3870
3871             # Issue the item
3872             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
3873
3874             # Fake fines cronjob on this checkout
3875             my ($fine) =
3876               CalcFine( $item, $patron->categorycode, $library->{branchcode},
3877                 $ten_days_ago, $now );
3878             UpdateFine(
3879                 {
3880                     issue_id       => $issue->issue_id,
3881                     itemnumber     => $item->itemnumber,
3882                     borrowernumber => $patron->borrowernumber,
3883                     amount         => $fine,
3884                     due            => output_pref($ten_days_ago)
3885                 }
3886             );
3887             my $overdue_fees = Koha::Account::Lines->search(
3888                 {
3889                     borrowernumber  => $patron->id,
3890                     itemnumber      => $item->itemnumber,
3891                     debit_type_code => 'OVERDUE'
3892                 }
3893             );
3894             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3895             my $overdue_fee = $overdue_fees->next;
3896             is( $overdue_fee->amount + 0,
3897                 10, 'The right OVERDUE amount is generated' );
3898             is( $overdue_fee->amountoutstanding + 0,
3899                 10,
3900                 'The right OVERDUE amountoutstanding is generated' );
3901
3902             # Simulate item marked as lost
3903             $item->itemlost(3)->store;
3904             C4::Circulation::LostItem( $item->itemnumber, 1 );
3905
3906             my $lost_fee_lines = Koha::Account::Lines->search(
3907                 {
3908                     borrowernumber  => $patron->id,
3909                     itemnumber      => $item->itemnumber,
3910                     debit_type_code => 'LOST'
3911                 }
3912             );
3913             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3914             my $lost_fee_line = $lost_fee_lines->next;
3915             is( $lost_fee_line->amount + 0,
3916                 $replacement_amount, 'The right LOST amount is generated' );
3917             is( $lost_fee_line->amountoutstanding + 0,
3918                 $replacement_amount,
3919                 'The right LOST amountoutstanding is generated' );
3920             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3921
3922             # Return lost item
3923             my ( $returned, $message ) =
3924               AddReturn( $item->barcode, $branchcode_false, undef, $five_days_ago );
3925
3926             $overdue_fee->discard_changes;
3927             is( $overdue_fee->amount + 0,
3928                 10, 'The OVERDUE amount is left intact' );
3929             is( $overdue_fee->amountoutstanding + 0,
3930                 10,
3931                 'The OVERDUE amountoutstanding is left intact' );
3932
3933             $lost_fee_line->discard_changes;
3934             is( $lost_fee_line->amount + 0,
3935                 $replacement_amount, 'The LOST amount is left intact' );
3936             is( $lost_fee_line->amountoutstanding + 0,
3937                 $replacement_amount,
3938                 'The LOST amountoutstanding is left intact' );
3939             # FIXME: Should we set the LOST fee status to 'FOUND' regardless of whether we're refunding or not?
3940             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3941         };
3942
3943         subtest 'lostreturn | refund' => sub {
3944             plan tests => 12;
3945
3946             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund });
3947
3948             my $item = $builder->build_sample_item(
3949                 {
3950                     replacementprice => $replacement_amount
3951                 }
3952             );
3953
3954             # Issue the item
3955             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
3956
3957             # Fake fines cronjob on this checkout
3958             my ($fine) =
3959               CalcFine( $item, $patron->categorycode, $library->{branchcode},
3960                 $ten_days_ago, $now );
3961             UpdateFine(
3962                 {
3963                     issue_id       => $issue->issue_id,
3964                     itemnumber     => $item->itemnumber,
3965                     borrowernumber => $patron->borrowernumber,
3966                     amount         => $fine,
3967                     due            => output_pref($ten_days_ago)
3968                 }
3969             );
3970             my $overdue_fees = Koha::Account::Lines->search(
3971                 {
3972                     borrowernumber  => $patron->id,
3973                     itemnumber      => $item->itemnumber,
3974                     debit_type_code => 'OVERDUE'
3975                 }
3976             );
3977             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3978             my $overdue_fee = $overdue_fees->next;
3979             is( $overdue_fee->amount + 0,
3980                 10, 'The right OVERDUE amount is generated' );
3981             is( $overdue_fee->amountoutstanding + 0,
3982                 10,
3983                 'The right OVERDUE amountoutstanding is generated' );
3984
3985             # Simulate item marked as lost
3986             $item->itemlost(3)->store;
3987             C4::Circulation::LostItem( $item->itemnumber, 1 );
3988
3989             my $lost_fee_lines = Koha::Account::Lines->search(
3990                 {
3991                     borrowernumber  => $patron->id,
3992                     itemnumber      => $item->itemnumber,
3993                     debit_type_code => 'LOST'
3994                 }
3995             );
3996             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3997             my $lost_fee_line = $lost_fee_lines->next;
3998             is( $lost_fee_line->amount + 0,
3999                 $replacement_amount, 'The right LOST amount is generated' );
4000             is( $lost_fee_line->amountoutstanding + 0,
4001                 $replacement_amount,
4002                 'The right LOST amountoutstanding is generated' );
4003             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4004
4005             # Return the lost item
4006             my ( undef, $message ) =
4007               AddReturn( $item->barcode, $branchcode_refund, undef, $five_days_ago );
4008
4009             $overdue_fee->discard_changes;
4010             is( $overdue_fee->amount + 0,
4011                 10, 'The OVERDUE amount is left intact' );
4012             is( $overdue_fee->amountoutstanding + 0,
4013                 10,
4014                 'The OVERDUE amountoutstanding is left intact' );
4015
4016             $lost_fee_line->discard_changes;
4017             is( $lost_fee_line->amount + 0,
4018                 $replacement_amount, 'The LOST amount is left intact' );
4019             is( $lost_fee_line->amountoutstanding + 0,
4020                 0,
4021                 'The LOST amountoutstanding is refunded' );
4022             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
4023         };
4024
4025         subtest 'lostreturn | restore' => sub {
4026             plan tests => 13;
4027
4028             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_restore });
4029
4030             my $item = $builder->build_sample_item(
4031                 {
4032                     replacementprice => $replacement_amount
4033                 }
4034             );
4035
4036             # Issue the item
4037             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode , $ten_days_ago);
4038
4039             # Fake fines cronjob on this checkout
4040             my ($fine) =
4041               CalcFine( $item, $patron->categorycode, $library->{branchcode},
4042                 $ten_days_ago, $now );
4043             UpdateFine(
4044                 {
4045                     issue_id       => $issue->issue_id,
4046                     itemnumber     => $item->itemnumber,
4047                     borrowernumber => $patron->borrowernumber,
4048                     amount         => $fine,
4049                     due            => output_pref($ten_days_ago)
4050                 }
4051             );
4052             my $overdue_fees = Koha::Account::Lines->search(
4053                 {
4054                     borrowernumber  => $patron->id,
4055                     itemnumber      => $item->itemnumber,
4056                     debit_type_code => 'OVERDUE'
4057                 }
4058             );
4059             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
4060             my $overdue_fee = $overdue_fees->next;
4061             is( $overdue_fee->amount + 0,
4062                 10, 'The right OVERDUE amount is generated' );
4063             is( $overdue_fee->amountoutstanding + 0,
4064                 10,
4065                 'The right OVERDUE amountoutstanding is generated' );
4066
4067             # Simulate item marked as lost
4068             $item->itemlost(3)->store;
4069             C4::Circulation::LostItem( $item->itemnumber, 1 );
4070
4071             my $lost_fee_lines = Koha::Account::Lines->search(
4072                 {
4073                     borrowernumber  => $patron->id,
4074                     itemnumber      => $item->itemnumber,
4075                     debit_type_code => 'LOST'
4076                 }
4077             );
4078             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
4079             my $lost_fee_line = $lost_fee_lines->next;
4080             is( $lost_fee_line->amount + 0,
4081                 $replacement_amount, 'The right LOST amount is generated' );
4082             is( $lost_fee_line->amountoutstanding + 0,
4083                 $replacement_amount,
4084                 'The right LOST amountoutstanding is generated' );
4085             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4086
4087             # Simulate refunding overdue fees upon marking item as lost
4088             my $overdue_forgive = $patron->account->add_credit(
4089                 {
4090                     amount     => 10.00,
4091                     user_id    => $manager->borrowernumber,
4092                     library_id => $branchcode_restore,
4093                     interface  => 'test',
4094                     type       => 'FORGIVEN',
4095                     item_id    => $item->itemnumber
4096                 }
4097             );
4098             $overdue_forgive->apply( { debits => [$overdue_fee] } );
4099             $overdue_fee->discard_changes;
4100             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
4101
4102             # Do nothing
4103             my ( undef, $message ) =
4104               AddReturn( $item->barcode, $branchcode_restore, undef, $five_days_ago );
4105
4106             $overdue_fee->discard_changes;
4107             is( $overdue_fee->amount + 0,
4108                 10, 'The OVERDUE amount is left intact' );
4109             is( $overdue_fee->amountoutstanding + 0,
4110                 10,
4111                 'The OVERDUE amountoutstanding is restored' );
4112
4113             $lost_fee_line->discard_changes;
4114             is( $lost_fee_line->amount + 0,
4115                 $replacement_amount, 'The LOST amount is left intact' );
4116             is( $lost_fee_line->amountoutstanding + 0,
4117                 0,
4118                 'The LOST amountoutstanding is refunded' );
4119             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
4120         };
4121
4122         subtest 'lostreturn | charge' => sub {
4123             plan tests => 16;
4124
4125             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_charge });
4126
4127             my $item = $builder->build_sample_item(
4128                 {
4129                     replacementprice => $replacement_amount
4130                 }
4131             );
4132
4133             # Issue the item
4134             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
4135
4136             # Fake fines cronjob on this checkout
4137             my ($fine) =
4138               CalcFine( $item, $patron->categorycode, $library->{branchcode},
4139                 $ten_days_ago, $now );
4140             UpdateFine(
4141                 {
4142                     issue_id       => $issue->issue_id,
4143                     itemnumber     => $item->itemnumber,
4144                     borrowernumber => $patron->borrowernumber,
4145                     amount         => $fine,
4146                     due            => output_pref($ten_days_ago)
4147                 }
4148             );
4149             my $overdue_fees = Koha::Account::Lines->search(
4150                 {
4151                     borrowernumber  => $patron->id,
4152                     itemnumber      => $item->itemnumber,
4153                     debit_type_code => 'OVERDUE'
4154                 }
4155             );
4156             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
4157             my $overdue_fee = $overdue_fees->next;
4158             is( $overdue_fee->amount + 0,
4159                 10, 'The right OVERDUE amount is generated' );
4160             is( $overdue_fee->amountoutstanding + 0,
4161                 10,
4162                 'The right OVERDUE amountoutstanding is generated' );
4163
4164             # Simulate item marked as lost
4165             $item->itemlost(3)->store;
4166             C4::Circulation::LostItem( $item->itemnumber, 1 );
4167
4168             my $lost_fee_lines = Koha::Account::Lines->search(
4169                 {
4170                     borrowernumber  => $patron->id,
4171                     itemnumber      => $item->itemnumber,
4172                     debit_type_code => 'LOST'
4173                 }
4174             );
4175             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
4176             my $lost_fee_line = $lost_fee_lines->next;
4177             is( $lost_fee_line->amount + 0,
4178                 $replacement_amount, 'The right LOST amount is generated' );
4179             is( $lost_fee_line->amountoutstanding + 0,
4180                 $replacement_amount,
4181                 'The right LOST amountoutstanding is generated' );
4182             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4183
4184             # Simulate refunding overdue fees upon marking item as lost
4185             my $overdue_forgive = $patron->account->add_credit(
4186                 {
4187                     amount     => 10.00,
4188                     user_id    => $manager->borrowernumber,
4189                     library_id => $branchcode_charge,
4190                     interface  => 'test',
4191                     type       => 'FORGIVEN',
4192                     item_id    => $item->itemnumber
4193                 }
4194             );
4195             $overdue_forgive->apply( { debits => [$overdue_fee] } );
4196             $overdue_fee->discard_changes;
4197             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
4198
4199             # Do nothing
4200             my ( undef, $message ) =
4201               AddReturn( $item->barcode, $branchcode_charge, undef, $five_days_ago );
4202
4203             $lost_fee_line->discard_changes;
4204             is( $lost_fee_line->amount + 0,
4205                 $replacement_amount, 'The LOST amount is left intact' );
4206             is( $lost_fee_line->amountoutstanding + 0,
4207                 0,
4208                 'The LOST amountoutstanding is refunded' );
4209             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
4210
4211             $overdue_fees = Koha::Account::Lines->search(
4212                 {
4213                     borrowernumber  => $patron->id,
4214                     itemnumber      => $item->itemnumber,
4215                     debit_type_code => 'OVERDUE'
4216                 },
4217                 {
4218                     order_by => { '-asc' => 'accountlines_id'}
4219                 }
4220             );
4221             is( $overdue_fees->count, 2, 'A second OVERDUE fee has been added' );
4222             $overdue_fee = $overdue_fees->next;
4223             is( $overdue_fee->amount + 0,
4224                 10, 'The original OVERDUE amount is left intact' );
4225             is( $overdue_fee->amountoutstanding + 0,
4226                 0,
4227                 'The original OVERDUE amountoutstanding is left as forgiven' );
4228             $overdue_fee = $overdue_fees->next;
4229             is( $overdue_fee->amount + 0,
4230                 5, 'The new OVERDUE amount is correct for the backdated return' );
4231             is( $overdue_fee->amountoutstanding + 0,
4232                 5,
4233                 'The new OVERDUE amountoutstanding is correct for the backdated return' );
4234         };
4235     };
4236 };
4237
4238 subtest '_FixOverduesOnReturn' => sub {
4239     plan tests => 14;
4240
4241     my $manager = $builder->build_object({ class => "Koha::Patrons" });
4242     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
4243
4244     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
4245
4246     my $branchcode  = $library2->{branchcode};
4247
4248     my $item = $builder->build_sample_item(
4249         {
4250             biblionumber     => $biblio->biblionumber,
4251             library          => $branchcode,
4252             replacementprice => 99.00,
4253             itype            => $itemtype,
4254         }
4255     );
4256
4257     my $patron = $builder->build( { source => 'Borrower' } );
4258
4259     ## Start with basic call, should just close out the open fine
4260     my $accountline = Koha::Account::Line->new(
4261         {
4262             borrowernumber => $patron->{borrowernumber},
4263             debit_type_code    => 'OVERDUE',
4264             status         => 'UNRETURNED',
4265             itemnumber     => $item->itemnumber,
4266             amount => 99.00,
4267             amountoutstanding => 99.00,
4268             interface => 'test',
4269         }
4270     )->store();
4271
4272     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
4273
4274     $accountline->_result()->discard_changes();
4275
4276     is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
4277     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4278     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
4279
4280     ## Run again, with exemptfine enabled
4281     $accountline->set(
4282         {
4283             debit_type_code    => 'OVERDUE',
4284             status         => 'UNRETURNED',
4285             amountoutstanding => 99.00,
4286         }
4287     )->store();
4288
4289     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
4290
4291     $accountline->_result()->discard_changes();
4292     my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'APPLY' })->next();
4293
4294     is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
4295     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4296     is( $accountline->status, 'RETURNED', 'Open fine ( account type OVERDUE ) has been set to returned ( status RETURNED )');
4297     is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
4298     is( $offset->amount + 0, -99, "Amount of offset is correct" );
4299     my $credit = $offset->credit;
4300     is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
4301     is( $credit->amount + 0, -99, "Credit amount is set correctly" );
4302     is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
4303
4304     # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
4305     $accountline->set(
4306         {
4307             debit_type_code    => 'OVERDUE',
4308             status         => 'UNRETURNED',
4309             amountoutstanding => 0.00,
4310         }
4311     )->store();
4312     $offset->delete;
4313
4314     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
4315
4316     $accountline->_result()->discard_changes();
4317     $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'CREATE' })->next();
4318     is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
4319     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4320     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
4321 };
4322
4323 subtest 'Set waiting flag' => sub {
4324     plan tests => 11;
4325
4326     my $library_1 = $builder->build( { source => 'Branch' } );
4327     my $patron_1  = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
4328     my $library_2 = $builder->build( { source => 'Branch' } );
4329     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
4330
4331     my $item = $builder->build_sample_item(
4332         {
4333             library      => $library_1->{branchcode},
4334         }
4335     );
4336
4337     set_userenv( $library_2 );
4338     my $reserve_id = AddReserve(
4339         {
4340             branchcode     => $library_2->{branchcode},
4341             borrowernumber => $patron_2->{borrowernumber},
4342             biblionumber   => $item->biblionumber,
4343             priority       => 1,
4344             itemnumber     => $item->itemnumber,
4345         }
4346     );
4347
4348     set_userenv( $library_1 );
4349     my $do_transfer = 1;
4350     my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
4351     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4352     my $hold = Koha::Holds->find( $reserve_id );
4353     is( $hold->found, 'T', 'Hold is in transit' );
4354
4355     my ( $status ) = CheckReserves($item);
4356     is( $status, 'Transferred', 'Hold is not waiting yet');
4357
4358     set_userenv( $library_2 );
4359     $do_transfer = 0;
4360     AddReturn( $item->barcode, $library_2->{branchcode} );
4361     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4362     $hold = Koha::Holds->find( $reserve_id );
4363     is( $hold->found, 'W', 'Hold is waiting' );
4364     ( $status ) = CheckReserves($item);
4365     is( $status, 'Waiting', 'Now the hold is waiting');
4366
4367     #Bug 21944 - Waiting transfer checked in at branch other than pickup location
4368     set_userenv( $library_1 );
4369     (undef, my $messages, undef, undef ) = AddReturn ( $item->barcode, $library_1->{branchcode} );
4370     $hold = Koha::Holds->find( $reserve_id );
4371     is( $hold->found, undef, 'Hold is no longer marked waiting' );
4372     is( $hold->priority, 1,  "Hold is now priority one again");
4373     is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
4374     is( $hold->itemnumber, $item->itemnumber, "Hold has retained its' itemnumber");
4375     is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
4376     is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
4377     is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
4378 };
4379
4380 subtest 'Cancel transfers on lost items' => sub {
4381     plan tests => 6;
4382
4383     my $library_to = $builder->build_object( { class => 'Koha::Libraries' } );
4384     my $item   = $builder->build_sample_item();
4385     my $holdingbranch = $item->holdingbranch;
4386     # Historic transfer (datearrived is defined)
4387     my $old_transfer = $builder->build_object(
4388         {
4389             class => 'Koha::Item::Transfers',
4390             value => {
4391                 itemnumber    => $item->itemnumber,
4392                 frombranch    => $holdingbranch,
4393                 tobranch      => $library_to->branchcode,
4394                 reason        => 'Manual',
4395                 datesent      => \'NOW()',
4396                 datearrived   => \'NOW()',
4397                 datecancelled => undef,
4398                 daterequested => \'NOW()'
4399             }
4400         }
4401     );
4402     # Queued transfer (datesent is undefined)
4403     my $transfer_1 = $builder->build_object(
4404         {
4405             class => 'Koha::Item::Transfers',
4406             value => {
4407                 itemnumber    => $item->itemnumber,
4408                 frombranch    => $holdingbranch,
4409                 tobranch      => $library_to->branchcode,
4410                 reason        => 'Manual',
4411                 datesent      => undef,
4412                 datearrived   => undef,
4413                 datecancelled => undef,
4414                 daterequested => \'NOW()'
4415             }
4416         }
4417     );
4418     # In transit transfer (datesent is defined, datearrived and datecancelled are both undefined)
4419     my $transfer_2 = $builder->build_object(
4420         {
4421             class => 'Koha::Item::Transfers',
4422             value => {
4423                 itemnumber    => $item->itemnumber,
4424                 frombranch    => $holdingbranch,
4425                 tobranch      => $library_to->branchcode,
4426                 reason        => 'Manual',
4427                 datesent      => \'NOW()',
4428                 datearrived   => undef,
4429                 datecancelled => undef,
4430                 daterequested => \'NOW()'
4431             }
4432         }
4433     );
4434
4435     # Simulate item being marked as lost
4436     $item->itemlost(1)->store;
4437     LostItem( $item->itemnumber, 'test', 1 );
4438
4439     $transfer_1->discard_changes;
4440     isnt($transfer_1->datecancelled, undef, "Queud transfer was cancelled upon item lost");
4441     is($transfer_1->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
4442     $transfer_2->discard_changes;
4443     isnt($transfer_2->datecancelled, undef, "Active transfer was cancelled upon item lost");
4444     is($transfer_2->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
4445     $old_transfer->discard_changes;
4446     is($old_transfer->datecancelled, undef, "Old transfers are unaffected");
4447     $item->discard_changes;
4448     is($item->holdingbranch, $holdingbranch, "Items holding branch remains unchanged");
4449 };
4450
4451 subtest 'CanBookBeIssued | is_overdue' => sub {
4452     plan tests => 3;
4453
4454     # Set a simple circ policy
4455     Koha::CirculationRules->set_rules(
4456         {
4457             categorycode => undef,
4458             branchcode   => undef,
4459             itemtype     => undef,
4460             rules        => {
4461                 maxissueqty     => 1,
4462                 reservesallowed => 25,
4463                 issuelength     => 14,
4464                 lengthunit      => 'days',
4465                 renewalsallowed => 1,
4466                 renewalperiod   => 7,
4467                 norenewalbefore => undef,
4468                 auto_renew      => 0,
4469                 fine            => .10,
4470                 chargeperiod    => 1,
4471             }
4472         }
4473     );
4474
4475     my $now   = dt_from_string()->truncate( to => 'day' );
4476     my $five_days_go = $now->clone->add( days => 5 );
4477     my $ten_days_go  = $now->clone->add( days => 10);
4478     my $library = $builder->build( { source => 'Branch' } );
4479     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
4480
4481     my $item = $builder->build_sample_item(
4482         {
4483             library      => $library->{branchcode},
4484         }
4485     );
4486
4487     my $issue = AddIssue( $patron, $item->barcode, $five_days_go ); # date due was 10d ago
4488     my $actualissue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
4489     is( output_pref({ str => $actualissue->date_due, dateonly => 1}), output_pref({ str => $five_days_go, dateonly => 1}), "First issue works");
4490     my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron, $item->barcode, $ten_days_go, undef, undef, undef);
4491     is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
4492     is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
4493 };
4494
4495 subtest 'ItemsDeniedRenewal rules are checked' => sub {
4496     plan tests => 4;
4497
4498     my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
4499     Koha::CirculationRules->set_rules(
4500         {
4501             categorycode => '*',
4502             itemtype     => '*',
4503             branchcode   => $idr_lib->branchcode,
4504             rules        => {
4505                 reservesallowed => 25,
4506                 issuelength     => 14,
4507                 lengthunit      => 'days',
4508                 renewalsallowed => 10,
4509                 renewalperiod   => 7,
4510                 norenewalbefore => undef,
4511                 auto_renew      => 0,
4512                 fine            => .10,
4513                 chargeperiod    => 1,
4514             }
4515         }
4516     );
4517
4518     my $allow_book = $builder->build_sample_item({
4519         homebranch => $idr_lib->branchcode,
4520         holdingbranch => $idr_lib->branchcode,
4521     });
4522
4523     my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
4524         branchcode => $idr_lib->branchcode,
4525         }
4526     });
4527     my $future = dt_from_string->add( days => 1 );
4528     my $issue = $builder->build_object(
4529         {
4530             class => 'Koha::Checkouts',
4531             value => {
4532                 returndate      => undef,
4533                 renewals_count  => 0,
4534                 auto_renew      => 0,
4535                 borrowernumber  => $idr_borrower->borrowernumber,
4536                 itemnumber      => $allow_book->itemnumber,
4537                 onsite_checkout => 0,
4538                 date_due        => $future,
4539             }
4540         }
4541     );
4542
4543     my $mock_item_class = Test::MockModule->new("Koha::Item");
4544     $mock_item_class->mock( 'is_denied_renewal', sub { return 1; } );
4545
4546     my ( $mayrenew, $error ) = CanBookBeRenewed( $idr_borrower, $issue );
4547     is( $mayrenew, 0, 'Renewal blocked when $item->is_denied_renewal returns true' );
4548     is( $error, 'item_denied_renewal', 'Renewal blocked when $item->is_denied_renewal returns true' );
4549
4550     $mock_item_class->unmock( 'is_denied_renewal' );
4551     $mock_item_class->mock( 'is_denied_renewal', sub { return 0; } );
4552
4553     ( $mayrenew, $error ) = CanBookBeRenewed( $idr_borrower, $issue );
4554     is( $mayrenew, 1, 'Renewal allowed when $item->is_denied_renewal returns false' );
4555     is( $error, undef, 'Renewal allowed when $item->is_denied_renewal returns false' );
4556
4557     $mock_item_class->unmock( 'is_denied_renewal' );
4558 };
4559
4560 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
4561     plan tests => 2;
4562
4563     t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
4564     my $library = $builder->build( { source => 'Branch' } );
4565     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
4566
4567     my $item = $builder->build_sample_item(
4568         {
4569             library      => $library->{branchcode},
4570         }
4571     );
4572
4573     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4574     is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4575     is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4576 };
4577
4578 subtest 'CanBookBeIssued | notforloan' => sub {
4579     plan tests => 2;
4580
4581     t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
4582
4583     my $library = $builder->build( { source => 'Branch' } );
4584     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
4585
4586     my $itemtype = $builder->build(
4587         {
4588             source => 'Itemtype',
4589             value  => { notforloan => undef, }
4590         }
4591     );
4592     my $item = $builder->build_sample_item(
4593         {
4594             library  => $library->{branchcode},
4595             itype    => $itemtype->{itemtype},
4596         }
4597     );
4598     $item->biblioitem->itemtype($itemtype->{itemtype})->store;
4599
4600     my ( $issuingimpossible, $needsconfirmation );
4601
4602
4603     subtest 'item-level_itypes = 1' => sub {
4604         plan tests => 6;
4605
4606         t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
4607         # Is for loan at item type and item level
4608         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4609         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4610         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4611
4612         # not for loan at item type level
4613         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
4614         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4615         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4616         is_deeply(
4617             $issuingimpossible,
4618             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
4619             'Item can not be issued, not for loan at item type level'
4620         );
4621
4622         # not for loan at item level
4623         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
4624         $item->notforloan( 1 )->store;
4625         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4626         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4627         is_deeply(
4628             $issuingimpossible,
4629             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
4630             'Item can not be issued, not for loan at item type level'
4631         );
4632     };
4633
4634     subtest 'item-level_itypes = 0' => sub {
4635         plan tests => 6;
4636
4637         t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
4638
4639         # We set another itemtype for biblioitem
4640         my $itemtype = $builder->build(
4641             {
4642                 source => 'Itemtype',
4643                 value  => { notforloan => undef, }
4644             }
4645         );
4646
4647         # for loan at item type and item level
4648         $item->notforloan(0)->store;
4649         $item->biblioitem->itemtype($itemtype->{itemtype})->store;
4650         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4651         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4652         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4653
4654         # not for loan at item type level
4655         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
4656         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4657         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4658         is_deeply(
4659             $issuingimpossible,
4660             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
4661             'Item can not be issued, not for loan at item type level'
4662         );
4663
4664         # not for loan at item level
4665         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
4666         $item->notforloan( 1 )->store;
4667         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4668         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4669         is_deeply(
4670             $issuingimpossible,
4671             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
4672             'Item can not be issued, not for loan at item type level'
4673         );
4674     };
4675
4676     # TODO test with AllowNotForLoanOverride = 1
4677 };
4678
4679 subtest 'CanBookBeIssued | recalls' => sub {
4680     plan tests => 3;
4681
4682     t::lib::Mocks::mock_preference("UseRecalls", 1);
4683     t::lib::Mocks::mock_preference("item-level_itypes", 1);
4684     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4685     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
4686     my $item = $builder->build_sample_item;
4687     Koha::CirculationRules->set_rules({
4688         branchcode => undef,
4689         itemtype => undef,
4690         categorycode => undef,
4691         rules => {
4692             recalls_allowed => 10,
4693         },
4694     });
4695
4696     # item-level recall
4697     my $recall = Koha::Recall->new(
4698         {   patron_id         => $patron1->borrowernumber,
4699             biblio_id         => $item->biblionumber,
4700             item_id           => $item->itemnumber,
4701             item_level        => 1,
4702             pickup_library_id => $patron1->branchcode,
4703         }
4704     )->store;
4705
4706     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
4707     is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed an item-level recall on this item" );
4708
4709     $recall->set_cancelled;
4710
4711     # biblio-level recall
4712     $recall = Koha::Recall->new(
4713         {   patron_id         => $patron1->borrowernumber,
4714             biblio_id         => $item->biblionumber,
4715             item_id           => undef,
4716             item_level        => 0,
4717             pickup_library_id => $patron1->branchcode,
4718         }
4719     )->store;
4720
4721     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
4722     is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed a biblio-level recall and this item is eligible to fill it" );
4723
4724     $recall->set_cancelled;
4725
4726     # biblio-level recall
4727     $recall = Koha::Recall->new(
4728         {   patron_id         => $patron1->borrowernumber,
4729             biblio_id         => $item->biblionumber,
4730             item_id           => undef,
4731             item_level        => 0,
4732             pickup_library_id => $patron1->branchcode,
4733         }
4734     )->store;
4735     $recall->set_waiting( { item => $item, expirationdate => dt_from_string() } );
4736
4737     my ( undef, undef, undef, $messages ) = CanBookBeIssued( $patron1, $item->barcode, undef, undef, undef, undef );
4738     is( $messages->{RECALLED}, $recall->id, "This book can be issued by this patron and they have placed a recall" );
4739
4740     $recall->set_cancelled;
4741 };
4742
4743 subtest 'CanBookBeIssued | bookings' => sub {
4744     plan tests => 4;
4745
4746     my $schema = Koha::Database->schema;
4747     $schema->storage->txn_begin;
4748
4749     my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } );
4750     my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } );
4751     my $item    = $builder->build_sample_item( { bookable => 1 } );
4752
4753     # item-level booking
4754     my $booking = Koha::Booking->new(
4755         {
4756             patron_id  => $patron1->borrowernumber,
4757             item_id    => $item->itemnumber,
4758             biblio_id  => $item->biblio->biblionumber,
4759             start_date => dt_from_string()->subtract( days => 1 ),
4760             end_date   => dt_from_string()->add( days => 6 ),
4761         }
4762     )->store();
4763
4764     # Booking encompasses proposed checkout
4765     my ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4766         $patron2, $item->barcode,
4767         dt_from_string()->add( days => 5 ),
4768         undef, undef, undef
4769     );
4770     is(
4771         $issuingimpossible->{BOOKED_TO_ANOTHER}->booking_id,
4772         $booking->booking_id,
4773         "Another patron has booked this item with a start date before the proposed due date"
4774     );
4775
4776     ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4777         $patron1, $item->barcode,
4778         dt_from_string()->add( days => 5 ),
4779         undef, undef, undef
4780     );
4781     is(
4782         $alerts->{BOOKED}->booking_id,
4783         $booking->booking_id, "Booked to this user"
4784     );
4785
4786     # Booking start will clash before issue due
4787     $booking->start_date( dt_from_string()->add( days => 3 ) )->store();
4788
4789     ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4790         $patron2, $item->barcode,
4791         dt_from_string()->add( days => 5 ),
4792         undef, undef, undef
4793     );
4794     is(
4795         $needsconfirmation->{BOOKED_TO_ANOTHER}->booking_id,
4796         $booking->booking_id,
4797         "Another patron has booked this item for a period starting before the proposed due date"
4798     );
4799
4800     ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4801         $patron1, $item->barcode,
4802         dt_from_string()->add( days => 5 ),
4803         undef, undef, undef
4804     );
4805     is(
4806         $needsconfirmation->{BOOKED_EARLY}->booking_id,
4807         $booking->booking_id,
4808         "Booked to this user, but they're collecting early"
4809     );
4810
4811     $schema->storage->txn_rollback;
4812 };
4813
4814 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
4815     plan tests => 1;
4816
4817     t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
4818     my $item = $builder->build_sample_item(
4819         {
4820             onloan => '2018-01-01',
4821         }
4822     );
4823
4824     AddReturn( $item->barcode, $item->homebranch );
4825     $item->discard_changes; # refresh
4826     is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
4827 };
4828
4829 subtest 'AddReturn | recalls' => sub {
4830     plan tests => 3;
4831
4832     t::lib::Mocks::mock_preference("UseRecalls", 1);
4833     t::lib::Mocks::mock_preference("item-level_itypes", 1);
4834     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4835     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
4836     my $item1 = $builder->build_sample_item;
4837     Koha::CirculationRules->set_rules({
4838         branchcode => undef,
4839         itemtype => undef,
4840         categorycode => undef,
4841         rules => {
4842             recalls_allowed => 10,
4843         },
4844     });
4845
4846     # this item can fill a recall with pickup at this branch
4847     AddIssue( $patron1, $item1->barcode );
4848     my $recall1 = Koha::Recall->new(
4849         {   patron_id         => $patron2->borrowernumber,
4850             biblio_id         => $item1->biblionumber,
4851             item_id           => $item1->itemnumber,
4852             item_level        => 1,
4853             pickup_library_id => $item1->homebranch,
4854         }
4855     )->store;
4856     my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
4857     is( $messages->{RecallFound}->id, $recall1->id, "Recall found" );
4858     $recall1->set_cancelled;
4859
4860     # this item can fill a recall but needs transfer
4861     AddIssue( $patron1, $item1->barcode );
4862     $recall1 = Koha::Recall->new(
4863         {   patron_id         => $patron2->borrowernumber,
4864             biblio_id         => $item1->biblionumber,
4865             item_id           => $item1->itemnumber,
4866             item_level        => 1,
4867             pickup_library_id => $patron2->branchcode,
4868         }
4869     )->store;
4870     ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
4871     is( $messages->{RecallNeedsTransfer}, $item1->homebranch, "Recall requiring transfer found" );
4872     $recall1->set_cancelled;
4873
4874     # this item is already in transit, do not ask to transfer
4875     AddIssue( $patron1, $item1->barcode );
4876     $recall1 = Koha::Recall->new(
4877         {   patron_id         => $patron2->borrowernumber,
4878             biblio_id         => $item1->biblionumber,
4879             item_id           => $item1->itemnumber,
4880             item_level        => 1,
4881             pickup_library_id => $patron2->branchcode,
4882         }
4883     )->store;
4884     $recall1->start_transfer;
4885     ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $patron2->branchcode );
4886     is( $messages->{TransferredRecall}->id, $recall1->id, "In transit recall found" );
4887     $recall1->set_cancelled;
4888 };
4889
4890 subtest 'AddReturn | bundles' => sub {
4891     plan tests => 1;
4892
4893     my $schema = Koha::Database->schema;
4894     $schema->storage->txn_begin;
4895
4896     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4897     my $host_item1 = $builder->build_sample_item;
4898     my $bundle_item1 = $builder->build_sample_item;
4899     $schema->resultset('ItemBundle')
4900       ->create(
4901         { host => $host_item1->itemnumber, item => $bundle_item1->itemnumber } );
4902
4903     my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $bundle_item1->barcode, $bundle_item1->homebranch );
4904     is($messages->{InBundle}->id, $host_item1->id, 'AddReturn returns InBundle host item when item is part of a bundle');
4905
4906     $schema->storage->txn_rollback;
4907 };
4908
4909 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
4910
4911     plan tests => 13;
4912
4913
4914     t::lib::Mocks::mock_preference('item-level_itypes', 1);
4915
4916     my $issuing_charges = 15;
4917     my $title   = 'A title';
4918     my $author  = 'Author, An';
4919     my $barcode = 'WHATARETHEODDS';
4920
4921     my $circ = Test::MockModule->new('C4::Circulation');
4922     $circ->mock(
4923         'GetIssuingCharges',
4924         sub {
4925             return $issuing_charges;
4926         }
4927     );
4928
4929     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
4930     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
4931     my $patron   = $builder->build_object({
4932         class => 'Koha::Patrons',
4933         value => { branchcode => $library->id }
4934     });
4935
4936     my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
4937     my $item_id = Koha::Item->new(
4938         {
4939             biblionumber     => $biblio->biblionumber,
4940             homebranch       => $library->id,
4941             holdingbranch    => $library->id,
4942             barcode          => $barcode,
4943             replacementprice => 23.00,
4944             itype            => $itemtype->id
4945         },
4946     )->store->itemnumber;
4947     my $item = Koha::Items->find( $item_id );
4948
4949     my $context = Test::MockModule->new('C4::Context');
4950     $context->mock( userenv => { branch => $library->id } );
4951
4952     # Check the item out
4953     AddIssue( $patron, $item->barcode );
4954
4955     throws_ok {
4956         AddRenewal(
4957             {
4958                 borrowernumber  => $patron->borrowernumber,
4959                 itemnumber      => $item->itemnumber,
4960                 branch          => $library->id,
4961                 lastreneweddate => { break => "the_renewal" }
4962             }
4963         );
4964     } 'Koha::Exceptions::Checkout::FailedRenewal', 'Exception is thrown when renewal update to issues fails';
4965
4966     t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
4967     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
4968     my %params_renewal = (
4969         timestamp => { -like => $date . "%" },
4970         module => "CIRCULATION",
4971         action => "RENEWAL",
4972     );
4973     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
4974     AddRenewal(
4975         {
4976             borrowernumber => $patron->id,
4977             itemnumber     => $item->id,
4978             branch         => $library->id
4979         }
4980     );
4981     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
4982     is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
4983
4984     my $checkouts = $patron->checkouts;
4985     # The following will fail if run on 00:00:00
4986     unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
4987
4988     my $lines = Koha::Account::Lines->search({
4989         borrowernumber => $patron->id,
4990         itemnumber     => $item->id
4991     });
4992
4993     is( $lines->count, 2 );
4994
4995     my $line = $lines->next;
4996     is( $line->debit_type_code, 'RENT',       'The issue of item with issuing charge generates an accountline of the correct type' );
4997     is( $line->branchcode,  $library->id, 'AddIssuingCharge correctly sets branchcode' );
4998     is( $line->description, '',     'AddIssue does not set a hardcoded description for the accountline' );
4999
5000     $line = $lines->next;
5001     is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
5002     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
5003     is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
5004
5005     t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
5006
5007     $context = Test::MockModule->new('C4::Context');
5008     $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
5009
5010     my $now = dt_from_string;
5011     $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
5012     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
5013     my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
5014     $sth->execute($item->id, $library->id);
5015     my ($old_stats_size) = $sth->fetchrow_array;
5016     AddRenewal(
5017         {
5018             borrowernumber => $patron->id,
5019             itemnumber     => $item->id,
5020             branch         => $library->id
5021         }
5022     );
5023     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
5024     $sth->execute($item->id, $library->id);
5025     my ($new_stats_size) = $sth->fetchrow_array;
5026     is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
5027     is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
5028
5029     AddReturn( $item->id, $library->id, undef, $date );
5030     AddIssue( $patron, $item->barcode, $now );
5031     AddRenewal(
5032         {
5033             borrowernumber => $patron->id,
5034             itemnumber     => $item->id,
5035             branch         => $library->id,
5036             skipfinecalc   => 1
5037         }
5038     );
5039     my $lines_skipped = Koha::Account::Lines->search({
5040         borrowernumber => $patron->id,
5041         itemnumber     => $item->id
5042     });
5043     is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
5044
5045 };
5046
5047 subtest 'AddRenewal() adds to renewals' => sub {
5048     plan tests => 5;
5049
5050     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
5051     my $patron   = $builder->build_object({
5052         class => 'Koha::Patrons',
5053         value => { branchcode => $library->id }
5054     });
5055
5056     my $item = $builder->build_sample_item();
5057
5058     set_userenv( $library->unblessed );
5059
5060     # Check the item out
5061     my $issue = AddIssue( $patron, $item->barcode );
5062     is(ref($issue), 'Koha::Checkout', 'Issue added');
5063
5064     # Renew item
5065     my $duedate = AddRenewal(
5066         {
5067             borrowernumber => $patron->id,
5068             itemnumber     => $item->id,
5069             branch         => $library->id,
5070             automatic      => 1
5071         }
5072     );
5073
5074     ok( $duedate, "Renewal added" );
5075
5076     my $renewals = Koha::Checkouts::Renewals->search({ checkout_id => $issue->issue_id });
5077     is($renewals->count, 1, 'One renewal added');
5078     my $THE_renewal = $renewals->next;
5079     is( $THE_renewal->renewer_id, C4::Context->userenv->{'number'}, 'Renewer recorded from context' );
5080     is( $THE_renewal->renewal_type, 'Automatic', 'AddRenewal "automatic" parameter sets renewal type to "Automatic"');
5081 };
5082
5083 subtest 'ProcessOfflinePayment() tests' => sub {
5084
5085     plan tests => 4;
5086
5087
5088     my $amount = 123;
5089
5090     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
5091     my $library = $builder->build_object({ class => 'Koha::Libraries' });
5092     my $result  = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
5093
5094     is( $result, 'Success.', 'The right string is returned' );
5095
5096     my $lines = $patron->account->lines;
5097     is( $lines->count, 1, 'line created correctly');
5098
5099     my $line = $lines->next;
5100     is( $line->amount+0, $amount * -1, 'amount picked from params' );
5101     is( $line->branchcode, $library->id, 'branchcode set correctly' );
5102
5103 };
5104
5105 subtest 'Incremented fee tests' => sub {
5106     plan tests => 19;
5107
5108     my $dt = dt_from_string();
5109     Time::Fake->offset( $dt->epoch );
5110
5111     t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
5112
5113     my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store;
5114
5115     $module->mock( 'userenv', sub { { branch => $library->id } } );
5116
5117     my $patron = $builder->build_object(
5118         {
5119             class => 'Koha::Patrons',
5120             value => { categorycode => $patron_category->{categorycode} }
5121         }
5122     );
5123
5124     my $itemtype = $builder->build_object(
5125         {
5126             class => 'Koha::ItemTypes',
5127             value => {
5128                 notforloan                   => undef,
5129                 rentalcharge                 => 0,
5130                 rentalcharge_daily           => 1,
5131                 rentalcharge_daily_calendar  => 0
5132             }
5133         }
5134     )->store;
5135
5136     my $item = $builder->build_sample_item(
5137         {
5138             library  => $library->id,
5139             itype    => $itemtype->id,
5140         }
5141     );
5142
5143     is( $itemtype->rentalcharge_daily + 0,1, 'Daily rental charge stored and retreived correctly' );
5144     is( $item->effective_itemtype, $itemtype->id, "Itemtype set correctly for item" );
5145
5146     my $now         = dt_from_string;
5147     my $dt_from     = $now->clone;
5148     my $dt_to       = $now->clone->add( days => 7 );
5149     my $dt_to_renew = $now->clone->add( days => 13 );
5150
5151     # Daily Tests
5152     my $issue =
5153       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5154     my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5155     is(
5156         $accountline->amount + 0,
5157         7,
5158         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
5159     );
5160     $accountline->delete();
5161     AddRenewal(
5162         {
5163             borrowernumber  => $patron->id,
5164             itemnumber      => $item->id,
5165             branch          => $library->id,
5166             datedue         => $dt_to_renew,
5167             lastreneweddate => $dt_to
5168         }
5169     );
5170     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5171     is(
5172         $accountline->amount + 0,
5173         6,
5174         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
5175     );
5176     $accountline->delete();
5177     $issue->delete();
5178
5179     t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
5180     $itemtype->rentalcharge_daily_calendar(1)->store();
5181     $issue =
5182       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5183     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5184     is(
5185         $accountline->amount + 0,
5186         7,
5187         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
5188     );
5189     $accountline->delete();
5190     AddRenewal(
5191         {
5192             borrowernumber  => $patron->id,
5193             itemnumber      => $item->id,
5194             branch          => $library->id,
5195             datedue         => $dt_to_renew,
5196             lastreneweddate => $dt_to
5197         }
5198     );
5199     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5200     is(
5201         $accountline->amount + 0,
5202         6,
5203         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
5204     );
5205     $accountline->delete();
5206     $issue->delete();
5207
5208     my $calendar = C4::Calendar->new( branchcode => $library->id );
5209     # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
5210     my $closed_day =
5211         ( $dt_from->day_of_week == 6 ) ? 0
5212       : ( $dt_from->day_of_week == 7 ) ? 1
5213       :                                  $dt_from->day_of_week + 1;
5214     my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
5215     $calendar->insert_week_day_holiday(
5216         weekday     => $closed_day,
5217         title       => 'Test holiday',
5218         description => 'Test holiday'
5219     );
5220     $issue =
5221       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5222     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5223     is(
5224         $accountline->amount + 0,
5225         6,
5226         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
5227     );
5228     $accountline->delete();
5229     AddRenewal(
5230         {
5231             borrowernumber  => $patron->id,
5232             itemnumber      => $item->id,
5233             branch          => $library->id,
5234             datedue         => $dt_to_renew,
5235             lastreneweddate => $dt_to
5236         }
5237     );
5238     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5239     is(
5240         $accountline->amount + 0,
5241         5,
5242         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
5243     );
5244     $accountline->delete();
5245     $issue->delete();
5246
5247     $itemtype->rentalcharge(2)->store;
5248     is( $itemtype->rentalcharge + 0, 2, 'Rental charge updated and retreived correctly' );
5249     $issue =
5250       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5251     my $accountlines =
5252       Koha::Account::Lines->search( { itemnumber => $item->id } );
5253     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly" );
5254     $accountlines->delete();
5255     AddRenewal(
5256         {
5257             borrowernumber  => $patron->id,
5258             itemnumber      => $item->id,
5259             branch          => $library->id,
5260             datedue         => $dt_to_renew,
5261             lastreneweddate => $dt_to
5262         }
5263     );
5264     $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
5265     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly, for renewal" );
5266     $accountlines->delete();
5267     $issue->delete();
5268     $itemtype->rentalcharge(0)->store;
5269     is( $itemtype->rentalcharge + 0, 0, 'Rental charge reset and retreived correctly' );
5270
5271     # Hourly
5272     Koha::CirculationRules->set_rule(
5273         {
5274             categorycode => $patron->categorycode,
5275             itemtype     => $itemtype->id,
5276             branchcode   => $library->id,
5277             rule_name    => 'lengthunit',
5278             rule_value   => 'hours',
5279         }
5280     );
5281
5282     $itemtype->rentalcharge_hourly('0.25')->store();
5283     is( $itemtype->rentalcharge_hourly, '0.25', 'Hourly rental charge stored and retreived correctly' );
5284
5285     $dt_to       = $now->clone->add( hours => 168 );
5286     $dt_to_renew = $now->clone->add( hours => 312 );
5287
5288     $itemtype->rentalcharge_hourly_calendar(0)->store();
5289     $issue =
5290       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5291     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5292     is(
5293         $accountline->amount + 0,
5294         42,
5295         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)"
5296     );
5297     $accountline->delete();
5298     AddRenewal(
5299         {
5300             borrowernumber  => $patron->id,
5301             itemnumber      => $item->id,
5302             branch          => $library->id,
5303             datedue         => $dt_to_renew,
5304             lastreneweddate => $dt_to
5305         }
5306     );
5307     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5308     is(
5309         $accountline->amount + 0,
5310         36,
5311         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)"
5312     );
5313     $accountline->delete();
5314     $issue->delete();
5315
5316     $itemtype->rentalcharge_hourly_calendar(1)->store();
5317     $issue =
5318       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5319     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5320     is(
5321         $accountline->amount + 0,
5322         36,
5323         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)"
5324     );
5325     $accountline->delete();
5326     AddRenewal(
5327         {
5328             borrowernumber  => $patron->id,
5329             itemnumber      => $item->id,
5330             branch          => $library->id,
5331             datedue         => $dt_to_renew,
5332             lastreneweddate => $dt_to
5333         }
5334     );
5335     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5336     is(
5337         $accountline->amount + 0,
5338         30,
5339         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u"
5340     );
5341     $accountline->delete();
5342     $issue->delete();
5343
5344     $calendar->delete_holiday( weekday => $closed_day );
5345     $issue =
5346       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5347     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5348     is(
5349         $accountline->amount + 0,
5350         42,
5351         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u"
5352     );
5353     $accountline->delete();
5354     AddRenewal(
5355         {
5356             borrowernumber  => $patron->id,
5357             itemnumber      => $item->id,
5358             branch          => $library->id,
5359             datedue         => $dt_to_renew,
5360             lastreneweddate => $dt_to
5361         }
5362     );
5363     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5364     is(
5365         $accountline->amount + 0,
5366         36,
5367         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)"
5368     );
5369     $accountline->delete();
5370     $issue->delete();
5371     Time::Fake->reset;
5372 };
5373
5374 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
5375     plan tests => 2;
5376
5377     t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
5378     t::lib::Mocks::mock_preference('item-level_itypes', 1);
5379
5380     my $library =
5381       $builder->build_object( { class => 'Koha::Libraries' } )->store;
5382     my $patron = $builder->build_object(
5383         {
5384             class => 'Koha::Patrons',
5385             value => { categorycode => $patron_category->{categorycode} }
5386         }
5387     );
5388
5389     my $itemtype = $builder->build_object(
5390         {
5391             class => 'Koha::ItemTypes',
5392             value => {
5393                 notforloan             => 0,
5394                 rentalcharge           => 0,
5395                 rentalcharge_daily => 0
5396             }
5397         }
5398     );
5399
5400     my $item = $builder->build_sample_item(
5401         {
5402             library    => $library->id,
5403             notforloan => 0,
5404             itemlost   => 0,
5405             withdrawn  => 0,
5406             itype      => $itemtype->id,
5407         }
5408     )->store;
5409
5410     my ( $issuingimpossible, $needsconfirmation );
5411     my $dt_from = dt_from_string();
5412     my $dt_due = $dt_from->clone->add( days => 3 );
5413
5414     $itemtype->rentalcharge(1)->store;
5415     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5416     is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
5417     $itemtype->rentalcharge('0')->store;
5418     $itemtype->rentalcharge_daily(1)->store;
5419     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5420     is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
5421     $itemtype->rentalcharge_daily('0')->store;
5422 };
5423
5424 subtest 'CanBookBeIssued & CircConfirmItemParts' => sub {
5425     plan tests => 1;
5426
5427     t::lib::Mocks::mock_preference('CircConfirmItemParts', 1);
5428
5429     my $patron = $builder->build_object(
5430         {
5431             class => 'Koha::Patrons',
5432             value => { categorycode => $patron_category->{categorycode} }
5433         }
5434     );
5435
5436     my $item = $builder->build_sample_item(
5437         {
5438             materials => 'includes DVD',
5439         }
5440     )->store;
5441
5442     my $dt_due = dt_from_string->add( days => 3 );
5443
5444     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5445     is_deeply( $needsconfirmation, { ADDITIONAL_MATERIALS => 'includes DVD' }, 'Item needs confirmation of additional parts' );
5446 };
5447
5448 subtest 'Do not return on renewal (LOST charge)' => sub {
5449     plan tests => 1;
5450
5451     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
5452     my $library = $builder->build_object( { class => "Koha::Libraries" } );
5453     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
5454     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
5455
5456     my $biblio = $builder->build_sample_biblio;
5457
5458     my $item = $builder->build_sample_item(
5459         {
5460             biblionumber     => $biblio->biblionumber,
5461             library          => $library->branchcode,
5462             replacementprice => 99.00,
5463             itype            => $itemtype,
5464         }
5465     );
5466
5467     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
5468     AddIssue( $patron, $item->barcode );
5469
5470     my $accountline = Koha::Account::Line->new(
5471         {
5472             borrowernumber    => $patron->borrowernumber,
5473             debit_type_code   => 'LOST',
5474             status            => undef,
5475             itemnumber        => $item->itemnumber,
5476             amount            => 12,
5477             amountoutstanding => 12,
5478             interface         => 'something',
5479         }
5480     )->store();
5481
5482     # AddRenewal doesn't call _FixAccountForLostAndFound
5483     AddIssue( $patron, $item->barcode );
5484
5485     is( $patron->checkouts->count, 1,
5486         'Renewal should not return the item even if a LOST payment has been made earlier'
5487     );
5488 };
5489
5490 subtest 'Filling a hold should cancel existing transfer' => sub {
5491     plan tests => 4;
5492
5493     t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
5494
5495     my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
5496     my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
5497     my $patron = $builder->build_object(
5498         {
5499             class => 'Koha::Patrons',
5500             value => {
5501                 categorycode => $patron_category->{categorycode},
5502                 branchcode => $libraryA->branchcode,
5503             }
5504         }
5505     );
5506
5507     my $item = $builder->build_sample_item({
5508         homebranch => $libraryB->branchcode,
5509     });
5510
5511     my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
5512     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
5513     AddReserve({
5514         branchcode     => $libraryA->branchcode,
5515         borrowernumber => $patron->borrowernumber,
5516         biblionumber   => $item->biblionumber,
5517         itemnumber     => $item->itemnumber
5518     });
5519     my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
5520     is( $reserves->count, 1, "Reserve is placed");
5521     ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
5522     my $reserve = $reserves->next;
5523     ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
5524     $reserve->discard_changes;
5525     ok( $reserve->found eq 'W', "Reserve is marked waiting" );
5526     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
5527 };
5528
5529 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddReturn' => sub {
5530
5531     plan tests => 4;
5532
5533     t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
5534     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
5535     my $patron  = $builder->build_object(
5536         {
5537             class => 'Koha::Patrons',
5538             value => { categorycode => $patron_category->{categorycode} }
5539         }
5540     );
5541
5542     my $biblionumber = $builder->build_sample_biblio(
5543         {
5544             branchcode => $library->branchcode,
5545         }
5546     )->biblionumber;
5547
5548     # And the circulation rule
5549     Koha::CirculationRules->search->delete;
5550     Koha::CirculationRules->set_rules(
5551         {
5552             categorycode => undef,
5553             itemtype     => undef,
5554             branchcode   => undef,
5555             rules        => {
5556                 issuelength => 14,
5557                 lengthunit  => 'days',
5558             }
5559         }
5560     );
5561     $builder->build(
5562         {
5563             source => 'CirculationRule',
5564             value  => {
5565                 branchcode   => undef,
5566                 categorycode => undef,
5567                 itemtype     => undef,
5568                 rule_name    => 'lostreturn',
5569                 rule_value   => 'refund'
5570             }
5571         }
5572     );
5573
5574     subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
5575         plan tests => 3;
5576
5577         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5578         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
5579
5580         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5581
5582         my $item = $builder->build_sample_item(
5583             {
5584                 biblionumber     => $biblionumber,
5585                 library          => $library->branchcode,
5586                 replacementprice => '42',
5587             }
5588         );
5589         my $issue = AddIssue( $patron, $item->barcode );
5590         LostItem( $item->itemnumber, 'cli', 0 );
5591         $item->_result->itemlost(1);
5592         $item->_result->itemlost_on( $lost_on );
5593         $item->_result->update();
5594
5595         my $a = Koha::Account::Lines->search(
5596             {
5597                 itemnumber     => $item->id,
5598                 borrowernumber => $patron->borrowernumber
5599             }
5600         )->next;
5601         ok( $a, "Found accountline for lost fee" );
5602         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5603         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5604         $a = $a->get_from_storage;
5605         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5606         $a->delete;
5607     };
5608
5609     subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
5610         plan tests => 3;
5611
5612         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5613         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5614
5615         my $lost_on = dt_from_string->subtract( days => 6 )->date;
5616
5617         my $item = $builder->build_sample_item(
5618             {
5619                 biblionumber     => $biblionumber,
5620                 library          => $library->branchcode,
5621                 replacementprice => '42',
5622             }
5623         );
5624         my $issue = AddIssue( $patron, $item->barcode );
5625         LostItem( $item->itemnumber, 'cli', 0 );
5626         $item->_result->itemlost(1);
5627         $item->_result->itemlost_on( $lost_on );
5628         $item->_result->update();
5629
5630         my $a = Koha::Account::Lines->search(
5631             {
5632                 itemnumber     => $item->id,
5633                 borrowernumber => $patron->borrowernumber
5634             }
5635         )->next;
5636         ok( $a, "Found accountline for lost fee" );
5637         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5638         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5639         $a = $a->get_from_storage;
5640         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5641         $a->delete;
5642     };
5643
5644     subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
5645         plan tests => 3;
5646
5647         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5648         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5649
5650         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5651
5652         my $item = $builder->build_sample_item(
5653             {
5654                 biblionumber     => $biblionumber,
5655                 library          => $library->branchcode,
5656                 replacementprice => '42',
5657             }
5658         );
5659         my $issue = AddIssue( $patron, $item->barcode );
5660         LostItem( $item->itemnumber, 'cli', 0 );
5661         $item->_result->itemlost(1);
5662         $item->_result->itemlost_on( $lost_on );
5663         $item->_result->update();
5664
5665         my $a = Koha::Account::Lines->search(
5666             {
5667                 itemnumber     => $item->id,
5668                 borrowernumber => $patron->borrowernumber
5669             }
5670         )->next;
5671         ok( $a, "Found accountline for lost fee" );
5672         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5673         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5674         $a = $a->get_from_storage;
5675         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5676         $a->delete;
5677     };
5678
5679     subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
5680         plan tests => 3;
5681
5682         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5683         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5684
5685         my $lost_on = dt_from_string->subtract( days => 8 )->date;
5686
5687         my $item = $builder->build_sample_item(
5688             {
5689                 biblionumber     => $biblionumber,
5690                 library          => $library->branchcode,
5691                 replacementprice => '42',
5692             }
5693         );
5694         my $issue = AddIssue( $patron, $item->barcode );
5695         LostItem( $item->itemnumber, 'cli', 0 );
5696         $item->_result->itemlost(1);
5697         $item->_result->itemlost_on( $lost_on );
5698         $item->_result->update();
5699
5700         my $a = Koha::Account::Lines->search(
5701             {
5702                 itemnumber     => $item->id,
5703                 borrowernumber => $patron->borrowernumber
5704             }
5705         );
5706         $a = $a->next;
5707         ok( $a, "Found accountline for lost fee" );
5708         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5709         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5710         $a = $a->get_from_storage;
5711         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5712         $a->delete;
5713     };
5714 };
5715
5716 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddIssue' => sub {
5717
5718     plan tests => 4;
5719
5720     t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
5721     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
5722     my $patron  = $builder->build_object(
5723         {
5724             class => 'Koha::Patrons',
5725             value => { categorycode => $patron_category->{categorycode} }
5726         }
5727     );
5728     my $patron2  = $builder->build_object(
5729         {
5730             class => 'Koha::Patrons',
5731             value => { categorycode => $patron_category->{categorycode} }
5732         }
5733     );
5734
5735     my $biblionumber = $builder->build_sample_biblio(
5736         {
5737             branchcode => $library->branchcode,
5738         }
5739     )->biblionumber;
5740
5741     # And the circulation rule
5742     Koha::CirculationRules->search->delete;
5743     Koha::CirculationRules->set_rules(
5744         {
5745             categorycode => undef,
5746             itemtype     => undef,
5747             branchcode   => undef,
5748             rules        => {
5749                 issuelength => 14,
5750                 lengthunit  => 'days',
5751             }
5752         }
5753     );
5754     $builder->build(
5755         {
5756             source => 'CirculationRule',
5757             value  => {
5758                 branchcode   => undef,
5759                 categorycode => undef,
5760                 itemtype     => undef,
5761                 rule_name    => 'lostreturn',
5762                 rule_value   => 'refund'
5763             }
5764         }
5765     );
5766
5767     subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
5768         plan tests => 3;
5769
5770         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5771         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
5772
5773         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5774
5775         my $item = $builder->build_sample_item(
5776             {
5777                 biblionumber     => $biblionumber,
5778                 library          => $library->branchcode,
5779                 replacementprice => '42',
5780             }
5781         );
5782         my $issue = AddIssue( $patron, $item->barcode );
5783         LostItem( $item->itemnumber, 'cli', 0 );
5784         $item->_result->itemlost(1);
5785         $item->_result->itemlost_on( $lost_on );
5786         $item->_result->update();
5787
5788         my $a = Koha::Account::Lines->search(
5789             {
5790                 itemnumber     => $item->id,
5791                 borrowernumber => $patron->borrowernumber
5792             }
5793         )->next;
5794         ok( $a, "Found accountline for lost fee" );
5795         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5796         $issue = AddIssue( $patron2, $item->barcode );
5797         $a = $a->get_from_storage;
5798         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5799         $a->delete;
5800         $issue->delete;
5801     };
5802
5803     subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
5804         plan tests => 3;
5805
5806         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5807         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5808
5809         my $lost_on = dt_from_string->subtract( days => 6 )->date;
5810
5811         my $item = $builder->build_sample_item(
5812             {
5813                 biblionumber     => $biblionumber,
5814                 library          => $library->branchcode,
5815                 replacementprice => '42',
5816             }
5817         );
5818         my $issue = AddIssue( $patron, $item->barcode );
5819         LostItem( $item->itemnumber, 'cli', 0 );
5820         $item->_result->itemlost(1);
5821         $item->_result->itemlost_on( $lost_on );
5822         $item->_result->update();
5823
5824         my $a = Koha::Account::Lines->search(
5825             {
5826                 itemnumber     => $item->id,
5827                 borrowernumber => $patron->borrowernumber
5828             }
5829         )->next;
5830         ok( $a, "Found accountline for lost fee" );
5831         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5832         $issue = AddIssue( $patron2, $item->barcode );
5833         $a = $a->get_from_storage;
5834         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5835         $a->delete;
5836     };
5837
5838     subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
5839         plan tests => 3;
5840
5841         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5842         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5843
5844         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5845
5846         my $item = $builder->build_sample_item(
5847             {
5848                 biblionumber     => $biblionumber,
5849                 library          => $library->branchcode,
5850                 replacementprice => '42',
5851             }
5852         );
5853         my $issue = AddIssue( $patron, $item->barcode );
5854         LostItem( $item->itemnumber, 'cli', 0 );
5855         $item->_result->itemlost(1);
5856         $item->_result->itemlost_on( $lost_on );
5857         $item->_result->update();
5858
5859         my $a = Koha::Account::Lines->search(
5860             {
5861                 itemnumber     => $item->id,
5862                 borrowernumber => $patron->borrowernumber
5863             }
5864         )->next;
5865         ok( $a, "Found accountline for lost fee" );
5866         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5867         $issue = AddIssue( $patron2, $item->barcode );
5868         $a = $a->get_from_storage;
5869         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5870         $a->delete;
5871     };
5872
5873     subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
5874         plan tests => 3;
5875
5876         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5877         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5878
5879         my $lost_on = dt_from_string->subtract( days => 8 )->date;
5880
5881         my $item = $builder->build_sample_item(
5882             {
5883                 biblionumber     => $biblionumber,
5884                 library          => $library->branchcode,
5885                 replacementprice => '42',
5886             }
5887         );
5888         my $issue = AddIssue( $patron, $item->barcode );
5889         LostItem( $item->itemnumber, 'cli', 0 );
5890         $item->_result->itemlost(1);
5891         $item->_result->itemlost_on( $lost_on );
5892         $item->_result->update();
5893
5894         my $a = Koha::Account::Lines->search(
5895             {
5896                 itemnumber     => $item->id,
5897                 borrowernumber => $patron->borrowernumber
5898             }
5899         );
5900         $a = $a->next;
5901         ok( $a, "Found accountline for lost fee" );
5902         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5903         $issue = AddIssue( $patron2, $item->barcode );
5904         $a = $a->get_from_storage;
5905         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5906         $a->delete;
5907     };
5908 };
5909
5910 subtest 'transferbook tests' => sub {
5911     plan tests => 9;
5912
5913     throws_ok
5914     { C4::Circulation::transferbook({}); }
5915     'Koha::Exceptions::MissingParameter',
5916     'Koha::Patron->store raises an exception on missing params';
5917
5918     throws_ok
5919     { C4::Circulation::transferbook({to_branch=>'anything'}); }
5920     'Koha::Exceptions::MissingParameter',
5921     'Koha::Patron->store raises an exception on missing params';
5922
5923     throws_ok
5924     { C4::Circulation::transferbook({from_branch=>'anything'}); }
5925     'Koha::Exceptions::MissingParameter',
5926     'Koha::Patron->store raises an exception on missing params';
5927
5928     my ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here'});
5929     is( $doreturn, 0, "No return without barcode");
5930     ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
5931     is( $messages->{BadBarcode}, undef, "No barcode passed means undef BadBarcode" );
5932
5933     ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here',barcode=>'BadBarcode'});
5934     is( $doreturn, 0, "No return without barcode");
5935     ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
5936     is( $messages->{BadBarcode}, 'BadBarcode', "No barcode passed means undef BadBarcode" );
5937
5938 };
5939
5940 subtest 'Checkout should correctly terminate a transfer' => sub {
5941     plan tests => 7;
5942
5943     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
5944     my $patron_1 = $builder->build_object(
5945         {
5946             class => 'Koha::Patrons',
5947             value => { branchcode => $library_1->branchcode }
5948         }
5949     );
5950     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
5951     my $patron_2 = $builder->build_object(
5952         {
5953             class => 'Koha::Patrons',
5954             value => { branchcode => $library_2->branchcode }
5955         }
5956     );
5957
5958     my $item = $builder->build_sample_item(
5959         {
5960             library => $library_1->branchcode,
5961         }
5962     );
5963
5964     t::lib::Mocks::mock_userenv( { branchcode => $library_1->branchcode } );
5965     my $reserve_id = AddReserve(
5966         {
5967             branchcode     => $library_2->branchcode,
5968             borrowernumber => $patron_2->borrowernumber,
5969             biblionumber   => $item->biblionumber,
5970             itemnumber     => $item->itemnumber,
5971             priority       => 1,
5972         }
5973     );
5974
5975     my $do_transfer = 1;
5976     ModItemTransfer( $item->itemnumber, $library_1->branchcode,
5977         $library_2->branchcode, 'Manual' );
5978     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
5979     GetOtherReserves( $item->itemnumber )
5980       ;    # To put the Reason, it's what does returns.pl...
5981     my $hold = Koha::Holds->find($reserve_id);
5982     is( $hold->found, 'T', 'Hold is in transit' );
5983     my $transfer = $item->get_transfer;
5984     is( $transfer->frombranch, $library_1->branchcode );
5985     is( $transfer->tobranch,   $library_2->branchcode );
5986     is( $transfer->reason,     'Reserve' );
5987
5988     t::lib::Mocks::mock_userenv( { branchcode => $library_2->branchcode } );
5989     AddIssue( $patron_1, $item->barcode );
5990     $transfer = $transfer->get_from_storage;
5991     isnt( $transfer->datearrived, undef );
5992     $hold = $hold->get_from_storage;
5993     is( $hold->found, undef, 'Hold is waiting' );
5994     is( $hold->priority, 1, );
5995 };
5996
5997 subtest 'AddIssue records staff who checked out item if appropriate' => sub  {
5998     plan tests => 2;
5999
6000     $module->mock( 'userenv', sub { { branch => $library->{id} } } );
6001
6002     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
6003     my $patron = $builder->build_object(
6004         {
6005             class => 'Koha::Patrons',
6006             value => { categorycode => $patron_category->{categorycode} }
6007         }
6008     );
6009     my $issuer = $builder->build_object(
6010         {
6011             class => 'Koha::Patrons',
6012             value => { categorycode => $patron_category->{categorycode} }
6013         }
6014     );
6015     my $item_1 = $builder->build_sample_item(
6016         {
6017             library  => $library->{branchcode}
6018         }
6019     );
6020
6021     my $item_2 = $builder->build_sample_item(
6022         {
6023             library  => $library->branchcode
6024         }
6025     );
6026
6027     $module->mock( 'userenv', sub { { branch => $library->id, number => $issuer->borrowernumber } } );
6028
6029     my $dt_from = dt_from_string();
6030     my $dt_to   = dt_from_string()->add( days => 7 );
6031
6032     my $issue_1 = AddIssue( $patron, $item_1->barcode, $dt_to, undef, $dt_from );
6033
6034     is( $issue_1->issuer, undef, "Staff who checked out the item not recorded when RecordStaffUserOnCheckout turned off" );
6035
6036     t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 1);
6037
6038     my $issue_2 =
6039       AddIssue( $patron, $item_2->barcode, $dt_to, undef, $dt_from );
6040
6041     is( $issue_2->issuer->borrowernumber, $issuer->borrowernumber, "Staff who checked out the item recorded when RecordStaffUserOnCheckout turned on" );
6042 };
6043
6044 subtest "Item's onloan value should be set if checked out item is checked out to a different patron" => sub {
6045     plan tests => 2;
6046
6047     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
6048     my $patron_1 = $builder->build_object(
6049         {
6050             class => 'Koha::Patrons',
6051             value => { branchcode => $library_1->branchcode }
6052         }
6053     );
6054     my $patron_2 = $builder->build_object(
6055         {
6056             class => 'Koha::Patrons',
6057             value => { branchcode => $library_1->branchcode }
6058         }
6059     );
6060
6061     my $item = $builder->build_sample_item(
6062         {
6063             library => $library_1->branchcode,
6064         }
6065     );
6066
6067     AddIssue( $patron_1, $item->barcode );
6068     ok( $item->get_from_storage->onloan, "Item's onloan column is set after initial checkout" );
6069     AddIssue( $patron_2, $item->barcode );
6070     ok( $item->get_from_storage->onloan, "Item's onloan column is set after second checkout" );
6071 };
6072
6073 subtest "updateWrongTransfer tests" => sub {
6074     plan tests => 5;
6075
6076     my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
6077     my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
6078     my $library3 = $builder->build_object( { class => 'Koha::Libraries' } );
6079     my $item     = $builder->build_sample_item(
6080         {
6081             homebranch    => $library1->branchcode,
6082             holdingbranch => $library2->branchcode,
6083             datelastseen  => undef
6084         }
6085     );
6086
6087     my $transfer = $builder->build_object(
6088         {
6089             class => 'Koha::Item::Transfers',
6090             value => {
6091                 itemnumber    => $item->itemnumber,
6092                 frombranch    => $library2->branchcode,
6093                 tobranch      => $library1->branchcode,
6094                 daterequested => dt_from_string,
6095                 datesent      => dt_from_string,
6096                 datecancelled => undef,
6097                 datearrived   => undef,
6098                 reason        => 'Manual'
6099             }
6100         }
6101     );
6102     is( ref($transfer), 'Koha::Item::Transfer', 'Mock transfer added' );
6103
6104     my $new_transfer = C4::Circulation::updateWrongTransfer($item->itemnumber, $library1->branchcode);
6105     is(ref($new_transfer), 'Koha::Item::Transfer', "updateWrongTransfer returns a 'Koha::Item::Transfer' object");
6106     ok( !$new_transfer->in_transit, "New transfer is NOT created as in transit (or cancelled)");
6107
6108     my $original_transfer = $transfer->get_from_storage;
6109     ok( defined($original_transfer->datecancelled), "Original transfer was cancelled");
6110     is( $original_transfer->cancellation_reason, 'WrongTransfer', "Original transfer cancellation reason is 'WrongTransfer'");
6111 };
6112
6113 subtest "SendCirculationAlert" => sub {
6114     plan tests => 3;
6115
6116     # When you would unsuspectingly call this unit test (with perl, not prove), you will be bitten by LOCK.
6117     # LOCK will commit changes and ruin your data
6118     # In order to prevent that, we will add KOHA_TESTING to $ENV; see further Circulation.pm
6119     $ENV{KOHA_TESTING} = 1;
6120
6121     # Setup branch, borrowr, and notice
6122     my $library = $builder->build_object({ class => 'Koha::Libraries' });
6123     set_userenv( $library->unblessed);
6124     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
6125     C4::Members::Messaging::SetMessagingPreference({
6126         borrowernumber => $patron->id,
6127         message_transport_types => ['sms'],
6128         message_attribute_id => 5
6129     });
6130     my $item = $builder->build_sample_item();
6131     my $checkin_notice = $builder->build_object({
6132         class => 'Koha::Notice::Templates',
6133         value =>{
6134             module => 'circulation',
6135             code => 'CHECKIN',
6136             branchcode => $library->branchcode,
6137             name => 'Test Checkin',
6138             is_html => 0,
6139             content => "Checkins:\n----\n[% biblio.title %]-[% old_checkout.issue_id %]\n----Thank you.",
6140             message_transport_type => 'sms',
6141             lang => 'default'
6142         }
6143     })->store;
6144
6145     # Checkout an item, mark it returned, generate a notice
6146     my $issue_1 = AddIssue( $patron, $item->barcode);
6147     MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
6148     C4::Circulation::SendCirculationAlert({
6149         type => 'CHECKIN',
6150         item => $item->unblessed,
6151         borrower => $patron->unblessed,
6152         branch => $library->branchcode,
6153         issue => $issue_1
6154     });
6155     my $notice = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'CHECKIN' });
6156     is($notice->content,"Checkins:\n".$item->biblio->title."-".$issue_1->id."\nThank you.", 'Letter generated with expected output on first checkin' );
6157     is($notice->to_address, $patron->smsalertnumber, "Letter has the correct to_address set to smsalertnumber for SMS type notices");
6158
6159     # Checkout an item, mark it returned, generate a notice
6160     my $issue_2 = AddIssue( $patron, $item->barcode);
6161     MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
6162     C4::Circulation::SendCirculationAlert({
6163         type => 'CHECKIN',
6164         item => $item->unblessed,
6165         borrower => $patron->unblessed,
6166         branch => $library->branchcode,
6167         issue => $issue_2
6168     });
6169     $notice->discard_changes();
6170     is($notice->content,"Checkins:\n".$item->biblio->title."-".$issue_1->id."\n".$item->biblio->title."-".$issue_2->id."\nThank you.", 'Letter appended with expected output on second checkin' );
6171
6172 };
6173
6174 subtest "GetSoonestRenewDate tests" => sub {
6175     plan tests => 6;
6176     Koha::CirculationRules->set_rule(
6177         {
6178             categorycode => undef,
6179             branchcode   => undef,
6180             itemtype     => undef,
6181             rule_name    => 'norenewalbefore',
6182             rule_value   => '7',
6183         }
6184     );
6185     my $patron = $builder->build_object(
6186         {
6187             class => 'Koha::Patrons',
6188             value => {
6189                 autorenew_checkouts => 1,
6190             }
6191         }
6192     );
6193     my $item = $builder->build_sample_item();
6194     my $issue = AddIssue( $patron, $item->barcode);
6195     my $datedue = dt_from_string( $issue->date_due() );
6196
6197     # Bug 14395
6198     # Test 'exact time' setting for syspref NoRenewalBeforePrecision
6199     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
6200     is(
6201         GetSoonestRenewDate( $patron, $issue ),
6202         $datedue->clone->add( days => -7 ),
6203         'Bug 14395: Renewals permitted 7 days before due date, as expected'
6204     );
6205
6206     # Bug 14395
6207     # Test 'date' setting for syspref NoRenewalBeforePrecision
6208     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6209     is(
6210         GetSoonestRenewDate( $patron, $issue ),
6211         $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
6212         'Bug 14395: Renewals permitted 7 days before due date, as expected'
6213     );
6214
6215
6216     Koha::CirculationRules->set_rule(
6217         {
6218             categorycode => undef,
6219             branchcode   => undef,
6220             itemtype     => undef,
6221             rule_name    => 'norenewalbefore',
6222             rule_value   => undef,
6223         }
6224     );
6225
6226     is(
6227         GetSoonestRenewDate( $patron, $issue ),
6228         dt_from_string,
6229         'Checkouts without auto-renewal can be renewed immediately if no norenewalbefore'
6230     );
6231
6232     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6233     $issue->auto_renew(1)->store;
6234     is(
6235         GetSoonestRenewDate( $patron, $issue, 1 ),
6236         $datedue->clone->truncate( to => 'day' ),
6237         'Checkouts with auto-renewal can be renewed earliest on due date if noautorenewalbefore'
6238     );
6239     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact' );
6240     is(
6241         GetSoonestRenewDate( $patron, $issue, 1 ),
6242         $datedue,
6243         'Checkouts with auto-renewal can be renewed earliest on due date if noautorenewalbefore'
6244     );
6245
6246     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6247     Koha::CirculationRules->set_rule(
6248         {
6249             categorycode => undef,
6250             branchcode   => undef,
6251             itemtype     => undef,
6252             rule_name    => 'norenewalbefore',
6253             rule_value   => 1,
6254         }
6255     );
6256     $issue->date_due(dt_from_string)->store;
6257     is(
6258         GetSoonestRenewDate( $patron, $issue ),
6259         dt_from_string->subtract( days => 1 )->truncate( to => 'day' ),
6260         'Checkouts with auto-renewal can be renewed 1 day before due date if no renewalbefore = 1 and precision = "date"'
6261     );
6262
6263 };
6264
6265 subtest "CanBookBeIssued + needsconfirmation message" => sub {
6266     plan tests => 4;
6267
6268     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
6269     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
6270     my $biblio = $builder->build_object({ class => 'Koha::Biblios' });
6271     my $biblioitem = $builder->build_object({ class => 'Koha::Biblioitems', value => { biblionumber => $biblio->biblionumber }});
6272     my $item = $builder->build_object({ class => 'Koha::Items' , value => { itype => $itemtype, biblionumber => $biblio->biblionumber }});
6273
6274     my $hold = $builder->build_object({ class => 'Koha::Holds', value => {
6275         biblionumber => $item->biblionumber,
6276         branchcode => $library->branchcode,
6277         itemnumber => undef,
6278         itemtype => undef,
6279         priority => 1,
6280         found => undef,
6281         suspend => 0,
6282         item_group_id => undef
6283     }});
6284
6285     my ( $error, $needsconfirmation, $alerts, $messages );
6286
6287     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6288     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold exists.");
6289
6290     $hold->priority(0)->store();
6291
6292     $hold->found("W")->store();
6293     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6294     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is waiting.");
6295
6296     $hold->found("T")->store();
6297     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6298     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is being transferred.");
6299
6300     $hold->found("P")->store();
6301     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6302     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is being processed.");
6303 };
6304
6305 subtest 'Tests for BlockReturnOfWithdrawnItems' => sub {
6306
6307     plan tests => 1;
6308
6309     t::lib::Mocks::mock_preference('BlockReturnOfWithdrawnItems', 1);
6310     t::lib::Mocks::mock_preference('RecordLocalUseOnReturn', 0);
6311     my $item = $builder->build_sample_item();
6312     $item->withdrawn(1)->itemlost(1)->store;
6313     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6314     is_deeply(
6315         \@return,
6316         [ 0, { NotIssued => $item->barcode, withdrawn => 1 }, undef, {} ], "Item returned as withdrawn, no other messages");
6317 };
6318
6319 subtest 'Tests for transfer not in transit' => sub {
6320
6321     plan tests => 2;
6322
6323
6324     # These tests are to ensure a 'pending' transfer, generated by
6325     # stock rotation, will be advanced when checked in
6326
6327     my $item = $builder->build_sample_item();
6328     my $transfer = $builder->build_object({ class => 'Koha::Item::Transfers', value => {
6329         itemnumber => $item->id,
6330         reason => 'StockrotationRepatriation',
6331         datesent => undef,
6332         frombranch => $item->homebranch,
6333     }});
6334     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6335     is_deeply(
6336         \@return,
6337         [ 0, { WasTransfered => $transfer->tobranch, TransferTrigger => 'StockrotationRepatriation', NotIssued => $item->barcode }, undef, {} ], "Item is reported to have been transferred");
6338
6339     $transfer->discard_changes;
6340     ok( $transfer->datesent, 'The datesent field is populated, i.e. transfer is initiated');
6341
6342 };
6343
6344 subtest 'Tests for RecordLocalUseOnReturn' => sub {
6345
6346     plan tests => 5;
6347
6348     t::lib::Mocks::mock_preference( 'RecordLocalUseOnReturn', 0 );
6349     my $item = $builder->build_sample_item();
6350
6351     my $item_2 = $builder->build_sample_item(
6352         {
6353             onloan => undef,
6354         }
6355     );
6356
6357     $item->withdrawn(1)->itemlost(1)->store;
6358     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6359     is_deeply(
6360         \@return,
6361         [ 0, { NotIssued => $item->barcode, withdrawn => 1 }, undef, {} ],
6362         "RecordLocalUSeOnReturn is off, no local use recorded"
6363     );
6364
6365     AddReturn( $item_2->barcode, $item_2->homebranch );
6366     $item_2->discard_changes;    # refresh
6367     is( $item_2->localuse, undef, 'Without RecordLocalUseOnReturn no localuse is recorded.' );
6368
6369     t::lib::Mocks::mock_preference( 'RecordLocalUseOnReturn', 1 );
6370     my @return2 = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6371     is_deeply(
6372         \@return2,
6373         [ 0, { NotIssued => $item->barcode, withdrawn => 1, LocalUse => 1 }, undef, {} ], "Local use is recorded"
6374     );
6375
6376     AddReturn( $item_2->barcode, $item_2->homebranch );
6377     $item_2->discard_changes;    # refresh
6378     is( $item_2->localuse, 1, 'With RecordLocalUseOnReturn localuse is recorded.' );
6379
6380     AddReturn( $item_2->barcode, $item_2->homebranch );
6381     $item_2->discard_changes;    # refresh
6382     is( $item_2->localuse, 2, 'With RecordLocalUseOnReturn localuse is recorded.' );
6383 };
6384
6385 subtest 'Test CanBookBeIssued param ignore_reserves (Bug 35322)' => sub {
6386     plan tests => 4;
6387
6388     my $homebranch    = $builder->build( { source => 'Branch' } );
6389     my $holdingbranch = $builder->build( { source => 'Branch' } );
6390     my $patron_1      = $builder->build_object(
6391         { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
6392     my $patron_2 = $builder->build_object(
6393         { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
6394
6395     my $item = $builder->build_sample_item(
6396         {
6397             homebranch    => $homebranch->{branchcode},
6398             holdingbranch => $holdingbranch->{branchcode},
6399         }
6400     );
6401
6402     Koha::CirculationRules->search()->delete();
6403     Koha::CirculationRules->set_rules(
6404         {
6405             categorycode => undef,
6406             itemtype     => undef,
6407             branchcode   => undef,
6408             rules        => {
6409                 reservesallowed => 25,
6410                 issuelength     => 14,
6411                 lengthunit      => 'days',
6412                 renewalsallowed => 1,
6413                 renewalperiod   => 7,
6414                 chargeperiod    => 1,
6415                 maxissueqty     => 20,
6416             }
6417         }
6418     );
6419
6420     my $reserve_id = AddReserve(
6421         {
6422             branchcode       => $homebranch,
6423             borrowernumber   => $patron_1->id,
6424             biblionumber     => $item->biblionumber,
6425             priority         => 1,
6426             reservation_date => dt_from_string,
6427             expiration_date  => dt_from_string,
6428             itemnumber       => $item->id,
6429             found            => 'W',
6430         }
6431     );
6432
6433     set_userenv($holdingbranch);
6434
6435     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode, undef, undef, 0 );
6436     is(
6437         keys(%$error) + keys(%$alerts), 0,
6438         'There should not be any errors or alerts (impossible)' . str( $error, $question, $alerts )
6439     );
6440     is( exists $question->{RESERVE_WAITING}, 1, 'RESERVE_WAITING is set' );
6441
6442     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode, undef, undef, 1 );
6443     is(
6444         keys(%$error) + keys(%$alerts), 0,
6445         'There should not be any errors or alerts (impossible)' . str( $error, $question, $alerts )
6446     );
6447     isnt( exists $question->{RESERVE_WAITING}, 1, 'RESERVE_WAITING is not set' );
6448
6449 };
6450
6451
6452 $schema->storage->txn_rollback;
6453 C4::Context->clear_syspref_cache();
6454 $branches = Koha::Libraries->search();
6455 for my $branch ( $branches->next ) {
6456     my $key = $branch->branchcode . "_holidays";
6457     $cache->clear_from_cache($key);
6458 }