]> git.koha-community.org Git - koha.git/blob - t/db_dependent/Circulation.t
Bug 36687: (RM follow-up) Fix unit tests
[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 => 74;
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 );
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         => 0,
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     my $pickup_library  = $builder->build_object( { class => 'Koha::Libraries' } );
1767
1768     # issue
1769     my $issue   = AddIssue( $renewing_patron, $item->barcode );
1770     my $datedue = dt_from_string( $issue->date_due() );
1771     is( defined $issue->date_due(), 1, "Item checked out, due date: " . $issue->date_due() );
1772
1773     # item-level booking
1774     my $booking = Koha::Booking->new(
1775         {
1776             patron_id         => $booked_patron->borrowernumber,
1777             pickup_library_id => $pickup_library->branchcode,
1778             item_id           => $item->itemnumber,
1779             biblio_id         => $item->biblio->biblionumber,
1780             start_date        => $datedue->clone()->add( days => 2 ),
1781             end_date          => $datedue->clone()->add( days => 10 ),
1782         }
1783     )->store();
1784
1785     # Proposed renewal would encroach on booking
1786     my ( $renewok, $error ) = CanBookBeRenewed( $renewing_patron, $issue, 0 );
1787     is( $renewok, 0,        "Renewal not allowed as it would mean the item was not returned before the next booking" );
1788     is( $error,   'booked', "Error is 'booked'" );
1789
1790     $schema->storage->txn_rollback;
1791 };
1792
1793 subtest "GetUpcomingDueIssues" => sub {
1794     plan tests => 12;
1795
1796     my $branch   = $library2->{branchcode};
1797
1798     #Create another record
1799     my $biblio2 = $builder->build_sample_biblio();
1800
1801     #Create third item
1802     my $item_1 = Koha::Items->find($reused_itemnumber_1);
1803     my $item_2 = Koha::Items->find($reused_itemnumber_2);
1804     my $item_3 = $builder->build_sample_item(
1805         {
1806             biblionumber     => $biblio2->biblionumber,
1807             library          => $branch,
1808             itype            => $itemtype,
1809         }
1810     );
1811
1812
1813     # Create a borrower
1814     my %a_borrower_data = (
1815         firstname =>  'Fridolyn',
1816         surname => 'SOMERS',
1817         categorycode => $patron_category->{categorycode},
1818         branchcode => $branch,
1819     );
1820
1821     my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1822     my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber );
1823
1824     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1825     my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1826     my $today = DateTime->today(time_zone => C4::Context->tz());
1827
1828     my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1829     my $datedue = dt_from_string( $issue->date_due() );
1830     my $issue_2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1831     my $datedue2 = dt_from_string( $issue->date_due() );
1832
1833     my $upcoming_dues;
1834
1835     # GetUpcomingDueIssues tests
1836     for my $i(0..1) {
1837         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1838         is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1839     }
1840
1841     #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1842     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1843     is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1844
1845     for my $i(3..5) {
1846         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1847         is ( scalar( @$upcoming_dues ), 1,
1848             "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1849     }
1850
1851     # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1852
1853     my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1854
1855     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1856     is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1857
1858     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1859     is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1860
1861     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1862     is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1863
1864     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 }  );
1865     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1866
1867     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1868     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1869
1870     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1871     is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1872
1873 };
1874
1875 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1876     my $branch   = $library2->{branchcode};
1877
1878     my $biblio = $builder->build_sample_biblio();
1879
1880     #Create third item
1881     my $item = $builder->build_sample_item(
1882         {
1883             biblionumber     => $biblio->biblionumber,
1884             library          => $branch,
1885             itype            => $itemtype,
1886         }
1887     );
1888
1889     # Create a borrower
1890     my %a_borrower_data = (
1891         firstname =>  'Kyle',
1892         surname => 'Hall',
1893         categorycode => $patron_category->{categorycode},
1894         branchcode => $branch,
1895     );
1896
1897     my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1898
1899     my $borrower = Koha::Patrons->find( $borrowernumber );
1900     my $issue = AddIssue( $borrower, $item->barcode );
1901     UpdateFine(
1902         {
1903             issue_id       => $issue->id(),
1904             itemnumber     => $item->itemnumber,
1905             borrowernumber => $borrowernumber,
1906             amount         => 0,
1907             type           => q{}
1908         }
1909     );
1910
1911     my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1912     my $count = $hr->{count};
1913
1914     is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1915 };
1916
1917 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1918     plan tests => 13;
1919     my $biblio = $builder->build_sample_biblio();
1920     my $item_1 = $builder->build_sample_item(
1921         {
1922             biblionumber     => $biblio->biblionumber,
1923             library          => $library2->{branchcode},
1924         }
1925     );
1926     my $item_2= $builder->build_sample_item(
1927         {
1928             biblionumber     => $biblio->biblionumber,
1929             library          => $library2->{branchcode},
1930             itype            => $item_1->effective_itemtype,
1931         }
1932     );
1933
1934     Koha::CirculationRules->set_rules(
1935         {
1936             categorycode => undef,
1937             itemtype     => $item_1->effective_itemtype,
1938             branchcode   => undef,
1939             rules        => {
1940                 reservesallowed => 25,
1941                 holds_per_record => 25,
1942                 issuelength     => 14,
1943                 lengthunit      => 'days',
1944                 renewalsallowed => 1,
1945                 renewalperiod   => 7,
1946                 norenewalbefore => undef,
1947                 auto_renew      => 0,
1948                 fine            => .10,
1949                 chargeperiod    => 1,
1950                 maxissueqty     => 20
1951             }
1952         }
1953     );
1954
1955     my $borrower1 = $builder->build_object(
1956         {
1957             class => 'Koha::Patrons',
1958             value => {
1959                 firstname    => 'Kyle',
1960                 surname      => 'Hall',
1961                 categorycode => $patron_category->{categorycode},
1962                 branchcode   => $library2->{branchcode},
1963             }
1964         }
1965     );
1966
1967     my $borrowernumber2 = $builder->build_object(
1968         {
1969             class => 'Koha::Patrons',
1970             value => {
1971                 firstname    => 'Chelsea',
1972                 surname      => 'Hall',
1973                 categorycode => $patron_category->{categorycode},
1974                 branchcode   => $library2->{branchcode},
1975
1976             }
1977         }
1978     )->borrowernumber;
1979
1980     my $patron_category_2 = $builder->build(
1981         {
1982             source => 'Category',
1983             value  => {
1984                 category_type                 => 'P',
1985                 enrolmentfee                  => 0,
1986                 BlockExpiredPatronOpacActions => -1, # Pick the pref value
1987             }
1988         }
1989     );
1990
1991     my $borrowernumber3 = $builder->build_object(
1992         {
1993             class => 'Koha::Patrons',
1994             value => {
1995                 firstname    => 'Carnegie',
1996                 surname      => 'Hall',
1997                 categorycode => $patron_category_2->{categorycode},
1998                 branchcode   => $library2->{branchcode},
1999             }
2000         }
2001     )->borrowernumber;
2002
2003     my $issue = AddIssue( $borrower1, $item_1->barcode );
2004
2005     my ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2006     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
2007
2008     AddReserve(
2009         {
2010             branchcode     => $library2->{branchcode},
2011             borrowernumber => $borrowernumber2,
2012             biblionumber   => $biblio->biblionumber,
2013             priority       => 1,
2014         }
2015     );
2016
2017     Koha::CirculationRules->set_rules(
2018         {
2019             categorycode => undef,
2020             itemtype     => $item_1->effective_itemtype,
2021             branchcode   => undef,
2022             rules        => {
2023                 onshelfholds => 0,
2024             }
2025         }
2026     );
2027     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
2028     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2029     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
2030
2031     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
2032     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2033     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
2034
2035     Koha::CirculationRules->set_rules(
2036         {
2037             categorycode => undef,
2038             itemtype     => $item_1->effective_itemtype,
2039             branchcode   => undef,
2040             rules        => {
2041                 onshelfholds => 1,
2042             }
2043         }
2044     );
2045     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
2046     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2047     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
2048
2049     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
2050     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2051     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
2052
2053     AddReserve(
2054         {
2055             branchcode     => $library2->{branchcode},
2056             borrowernumber => $borrowernumber3,
2057             biblionumber   => $biblio->biblionumber,
2058             priority       => 1,
2059         }
2060     );
2061
2062     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2063     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' );
2064
2065     my $item_3= $builder->build_sample_item(
2066         {
2067             biblionumber     => $biblio->biblionumber,
2068             library          => $library2->{branchcode},
2069             itype            => $item_1->effective_itemtype,
2070         }
2071     );
2072
2073     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2074     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' );
2075
2076     Koha::CirculationRules->set_rules(
2077         {
2078             categorycode => $patron_category_2->{categorycode},
2079             itemtype     => $item_1->effective_itemtype,
2080             branchcode   => undef,
2081             rules        => {
2082                 reservesallowed => 0,
2083             }
2084         }
2085     );
2086
2087     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2088     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' );
2089
2090     Koha::CirculationRules->set_rules(
2091         {
2092             categorycode => $patron_category_2->{categorycode},
2093             itemtype     => $item_1->effective_itemtype,
2094             branchcode   => undef,
2095             rules        => {
2096                 reservesallowed => 25,
2097             }
2098         }
2099     );
2100
2101     # Setting item not checked out to be not for loan but holdable
2102     $item_2->notforloan(-1)->store;
2103
2104     ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2105     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' );
2106
2107     my $mock_circ = Test::MockModule->new("C4::Circulation");
2108     $mock_circ->mock( CanItemBeReserved => sub {
2109         warn "Checked";
2110         return { status => 'no' }
2111     } );
2112
2113     $item_2->notforloan(0)->store;
2114     $item_3->delete();
2115     # Two items total, one item available, one issued, two holds on record
2116
2117     warnings_are{
2118        ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2119     } [], "CanItemBeReserved not called when there are more possible holds than available items";
2120     is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
2121
2122     $item_3 = $builder->build_sample_item(
2123         {
2124             biblionumber     => $biblio->biblionumber,
2125             library          => $library2->{branchcode},
2126             itype            => $item_1->effective_itemtype,
2127         }
2128     );
2129
2130     Koha::CirculationRules->set_rules(
2131         {
2132             categorycode => undef,
2133             itemtype     => $item_1->effective_itemtype,
2134             branchcode   => undef,
2135             rules        => {
2136                 reservesallowed => 0,
2137             }
2138         }
2139     );
2140
2141     warnings_are{
2142        ( $renewokay, $error ) = CanBookBeRenewed( $borrower1, $issue );
2143     } ["Checked","Checked"], "CanItemBeReserved only called once per available item if it returns a negative result for all items for a borrower";
2144     is( $renewokay, 0, 'Borrower cannot renew when there are more holds than available items' );
2145
2146 };
2147
2148 {
2149     # Don't allow renewing onsite checkout
2150     my $branch   = $library->{branchcode};
2151
2152     #Create another record
2153     my $biblio = $builder->build_sample_biblio();
2154
2155     my $item = $builder->build_sample_item(
2156         {
2157             biblionumber     => $biblio->biblionumber,
2158             library          => $branch,
2159             itype            => $itemtype,
2160         }
2161     );
2162
2163     my $borrower = $builder->build_object(
2164         {
2165             class => 'Koha::Patrons',
2166             value => {
2167                 firstname =>  'fn',
2168                 surname => 'dn',
2169                 categorycode => $patron_category->{categorycode},
2170                 branchcode => $branch,
2171             }
2172         }
2173     );
2174
2175     my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
2176     my ( $renewed, $error ) = CanBookBeRenewed( $borrower, $issue );
2177     is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
2178     is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
2179 }
2180
2181 {
2182     my $library = $builder->build({ source => 'Branch' });
2183
2184     my $biblio = $builder->build_sample_biblio();
2185
2186     my $item = $builder->build_sample_item(
2187         {
2188             biblionumber     => $biblio->biblionumber,
2189             library          => $library->{branchcode},
2190             itype            => $itemtype,
2191         }
2192     );
2193     my $patron = $builder->build_object( { class => 'Koha::Patrons',  value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2194
2195     my $issue = AddIssue( $patron, $item->barcode );
2196     UpdateFine(
2197         {
2198             issue_id       => $issue->id,
2199             itemnumber     => $item->itemnumber,
2200             borrowernumber => $patron->borrowernumber,
2201             amount         => 1,
2202             type           => q{}
2203         }
2204     );
2205     UpdateFine(
2206         {
2207             issue_id       => $issue->id,
2208             itemnumber     => $item->itemnumber,
2209             borrowernumber => $patron->borrowernumber,
2210             amount         => 2,
2211             type           => q{}
2212         }
2213     );
2214     is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
2215 }
2216
2217 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
2218     plan tests => 24;
2219
2220     my $homebranch    = $builder->build( { source => 'Branch' } );
2221     my $holdingbranch = $builder->build( { source => 'Branch' } );
2222     my $otherbranch   = $builder->build( { source => 'Branch' } );
2223     my $patron_1      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2224     my $patron_2      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2225
2226     my $item = $builder->build_sample_item(
2227         {
2228             homebranch    => $homebranch->{branchcode},
2229             holdingbranch => $holdingbranch->{branchcode},
2230         }
2231     );
2232     Koha::CirculationRules->set_rules(
2233         {
2234             categorycode => undef,
2235             itemtype     => $item->effective_itemtype,
2236             branchcode   => undef,
2237             rules        => {
2238                 reservesallowed => 25,
2239                 issuelength     => 14,
2240                 lengthunit      => 'days',
2241                 renewalsallowed => 1,
2242                 renewalperiod   => 7,
2243                 norenewalbefore => undef,
2244                 auto_renew      => 0,
2245                 fine            => .10,
2246                 chargeperiod    => 1,
2247                 maxissueqty     => 20
2248             }
2249         }
2250     );
2251
2252     set_userenv($holdingbranch);
2253
2254     my $issue = AddIssue( $patron_1, $item->barcode );
2255     is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
2256
2257     my ( $error, $question, $alerts );
2258
2259     # AllowReturnToBranch == anywhere
2260     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2261     ## Test that unknown barcodes don't generate internal server errors
2262     set_userenv($homebranch);
2263     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
2264     ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
2265     ## Can be issued from homebranch
2266     set_userenv($homebranch);
2267     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2268     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2269     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2270     ## Can be issued from holdingbranch
2271     set_userenv($holdingbranch);
2272     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2273     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2274     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2275     ## Can be issued from another branch
2276     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2277     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2278     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2279
2280     # AllowReturnToBranch == holdingbranch
2281     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
2282     ## Cannot be issued from homebranch
2283     set_userenv($homebranch);
2284     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2285     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2286     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2287     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
2288     ## Can be issued from holdinbranch
2289     set_userenv($holdingbranch);
2290     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2291     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2292     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2293     ## Cannot be issued from another branch
2294     set_userenv($otherbranch);
2295     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2296     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2297     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2298     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
2299
2300     # AllowReturnToBranch == homebranch
2301     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
2302     ## Can be issued from holdinbranch
2303     set_userenv($homebranch);
2304     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2305     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2306     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
2307     ## Cannot be issued from holdinbranch
2308     set_userenv($holdingbranch);
2309     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2310     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2311     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2312     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
2313     ## Cannot be issued from holdinbranch
2314     set_userenv($otherbranch);
2315     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode );
2316     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
2317     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
2318     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
2319
2320     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
2321 };
2322
2323 subtest 'AddIssue & AllowReturnToBranch' => sub {
2324     plan tests => 9;
2325
2326     my $homebranch    = $builder->build( { source => 'Branch' } );
2327     my $holdingbranch = $builder->build( { source => 'Branch' } );
2328     my $otherbranch   = $builder->build( { source => 'Branch' } );
2329
2330     my $patron_1  = $builder->build_object({
2331         class => 'Koha::Patrons',
2332         value => { categorycode => $patron_category->{categorycode} }
2333     });
2334
2335     my $patron_2  = $builder->build_object({
2336         class => 'Koha::Patrons',
2337         value => { categorycode => $patron_category->{categorycode} }
2338     });
2339
2340
2341     my $item = $builder->build_sample_item(
2342         {
2343             homebranch    => $homebranch->{branchcode},
2344             holdingbranch => $holdingbranch->{branchcode},
2345         }
2346     );
2347
2348     set_userenv($holdingbranch);
2349
2350     my $ref_issue = 'Koha::Checkout';
2351     my $issue = AddIssue( $patron_1, $item->barcode );
2352
2353     my ( $error, $question, $alerts );
2354
2355     # AllowReturnToBranch == homebranch
2356     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
2357     ## Can be issued from homebranch
2358     set_userenv($homebranch);
2359     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
2360     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2361     ## Can be issued from holdinbranch
2362     set_userenv($holdingbranch);
2363     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
2364     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2365     ## Can be issued from another branch
2366     set_userenv($otherbranch);
2367     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
2368     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2369
2370     # AllowReturnToBranch == holdinbranch
2371     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
2372     ## Cannot be issued from homebranch
2373     set_userenv($homebranch);
2374     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
2375     ## Can be issued from holdingbranch
2376     set_userenv($holdingbranch);
2377     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
2378     set_userenv($holdingbranch); AddIssue( $patron_1, $item->barcode ); # Reinsert the original issue
2379     ## Cannot be issued from another branch
2380     set_userenv($otherbranch);
2381     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
2382
2383     # AllowReturnToBranch == homebranch
2384     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
2385     ## Can be issued from homebranch
2386     set_userenv($homebranch);
2387     is ( ref( AddIssue( $patron_2, $item->barcode ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
2388     AddIssue( $patron_1, $item->barcode ); # Re-issue to patron 1
2389     ## Cannot be issued from holdinbranch
2390     set_userenv($holdingbranch);
2391     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
2392     ## Cannot be issued from another branch
2393     set_userenv($otherbranch);
2394     is ( ref( AddIssue( $patron_2, $item->barcode ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
2395     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
2396 };
2397
2398 subtest 'AddIssue | recalls' => sub {
2399     plan tests => 3;
2400
2401     t::lib::Mocks::mock_preference("UseRecalls", 1);
2402     t::lib::Mocks::mock_preference("item-level_itypes", 1);
2403     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
2404     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
2405     my $item = $builder->build_sample_item;
2406     Koha::CirculationRules->set_rules({
2407         branchcode => undef,
2408         itemtype => undef,
2409         categorycode => undef,
2410         rules => {
2411             recalls_allowed => 10,
2412         },
2413     });
2414
2415     # checking out item that they have recalled
2416     my $recall1 = Koha::Recall->new(
2417         {   patron_id         => $patron1->borrowernumber,
2418             biblio_id         => $item->biblionumber,
2419             item_id           => $item->itemnumber,
2420             item_level        => 1,
2421             pickup_library_id => $patron1->branchcode,
2422         }
2423     )->store;
2424     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall1->id } );
2425     $recall1 = Koha::Recalls->find( $recall1->id );
2426     is( $recall1->fulfilled, 1, 'Recall was fulfilled when patron checked out item' );
2427     AddReturn( $item->barcode, $item->homebranch );
2428
2429     # this item is has a recall request. cancel recall
2430     my $recall2 = Koha::Recall->new(
2431         {   patron_id         => $patron2->borrowernumber,
2432             biblio_id         => $item->biblionumber,
2433             item_id           => $item->itemnumber,
2434             item_level        => 1,
2435             pickup_library_id => $patron2->branchcode,
2436         }
2437     )->store;
2438     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall2->id, cancel_recall => 'cancel' } );
2439     $recall2 = Koha::Recalls->find( $recall2->id );
2440     is( $recall2->cancelled, 1, 'Recall was cancelled when patron checked out item' );
2441     AddReturn( $item->barcode, $item->homebranch );
2442
2443     # this item is waiting to fulfill a recall. revert recall
2444     my $recall3 = Koha::Recall->new(
2445         {   patron_id         => $patron2->borrowernumber,
2446             biblio_id         => $item->biblionumber,
2447             item_id           => $item->itemnumber,
2448             item_level        => 1,
2449             pickup_library_id => $patron2->branchcode,
2450         }
2451     )->store;
2452     $recall3->set_waiting;
2453     AddIssue( $patron1, $item->barcode, undef, undef, undef, undef, { recall_id => $recall3->id, cancel_recall => 'revert' } );
2454     $recall3 = Koha::Recalls->find( $recall3->id );
2455     is( $recall3->requested, 1, 'Recall was reverted from waiting when patron checked out item' );
2456     AddReturn( $item->barcode, $item->homebranch );
2457 };
2458
2459 subtest 'AddIssue & illrequests.due_date' => sub {
2460     plan tests => 2;
2461
2462     t::lib::Mocks::mock_preference( 'ILLModule', 1 );
2463     my $library = $builder->build( { source => 'Branch' } );
2464     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2465     my $item = $builder->build_sample_item();
2466
2467     set_userenv($library);
2468
2469     my $custom_date_due = '4999-12-18 12:34:56';
2470     my $expected_date_due = '4999-12-18 23:59:00';
2471     my $illrequest = Koha::ILL::Request->new({
2472         borrowernumber => $patron->borrowernumber,
2473         biblio_id => $item->biblionumber,
2474         branchcode => $library->{'branchcode'},
2475         due_date => $custom_date_due,
2476     })->store;
2477
2478     my $issue = AddIssue( $patron, $item->barcode );
2479     is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
2480
2481     $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2482     $item = $builder->build_sample_item();
2483     $custom_date_due = '4999-12-19';
2484     $expected_date_due = '4999-12-19 23:59:00';
2485     $illrequest = Koha::ILL::Request->new({
2486         borrowernumber => $patron->borrowernumber,
2487         biblio_id => $item->biblionumber,
2488         branchcode => $library->{'branchcode'},
2489         due_date => $custom_date_due,
2490     })->store;
2491
2492     $issue = AddIssue( $patron, $item->barcode );
2493     is( $issue->date_due, $expected_date_due, 'Custom illrequest date due has been set for this issue');
2494 };
2495
2496 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
2497     plan tests => 8;
2498
2499     my $library = $builder->build( { source => 'Branch' } );
2500     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2501     my $item_1 = $builder->build_sample_item(
2502         {
2503             library => $library->{branchcode},
2504         }
2505     );
2506     my $item_2 = $builder->build_sample_item(
2507         {
2508             library => $library->{branchcode},
2509         }
2510     );
2511     Koha::CirculationRules->set_rules(
2512         {
2513             categorycode => undef,
2514             itemtype     => undef,
2515             branchcode   => $library->{branchcode},
2516             rules        => {
2517                 reservesallowed => 25,
2518                 issuelength     => 14,
2519                 lengthunit      => 'days',
2520                 renewalsallowed => 1,
2521                 renewalperiod   => 7,
2522                 norenewalbefore => undef,
2523                 auto_renew      => 0,
2524                 fine            => .10,
2525                 chargeperiod    => 1,
2526                 maxissueqty     => 20
2527             }
2528         }
2529     );
2530
2531     my ( $error, $question, $alerts );
2532
2533     # Patron cannot issue item_1, they have overdues
2534     my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
2535     my $issue = AddIssue( $patron, $item_1->barcode, $yesterday );    # Add an overdue
2536
2537     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
2538     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2539     is( keys(%$error) + keys(%$alerts),  0, 'No key for error and alert' . str($error, $question, $alerts) );
2540     is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
2541
2542     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
2543     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2544     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2545     is( $error->{USERBLOCKEDOVERDUE},      1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
2546
2547     # Patron cannot issue item_1, they are debarred
2548     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
2549     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
2550     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2551     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2552     is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
2553
2554     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
2555     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2556     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
2557     is( $error->{USERBLOCKEDNOENDDATE},    '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
2558 };
2559
2560 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
2561     plan tests => 13;
2562
2563     my $library           = $builder->build_object( { class => 'Koha::Libraries' } );
2564     my $patron_category_x = $builder->build_object(
2565         {
2566             class => 'Koha::Patron::Categories',
2567             value => { category_type => 'X' }
2568         }
2569     );
2570     my $patron = $builder->build_object(
2571         {
2572             class => 'Koha::Patrons',
2573             value => {
2574                 categorycode  => $patron_category_x->categorycode,
2575                 gonenoaddress => undef,
2576                 lost          => undef,
2577                 debarred      => undef,
2578                 borrowernotes => ""
2579             }
2580         }
2581     );
2582     my $item_1 = $builder->build_sample_item(
2583         {
2584             library => $library->{branchcode},
2585         }
2586     );
2587
2588     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->barcode );
2589     is(
2590         $error->{STATS}, 1,
2591         '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)'
2592     );
2593
2594     my $stat = Koha::Statistics->search( { itemnumber => $item_1->itemnumber } )->next;
2595     is( $stat->branch,         C4::Context->userenv->{'branch'}, 'Recorded a branch' );
2596     is( $stat->type,           'localuse',                       'Recorded type as localuse' );
2597     is( $stat->itemnumber,     $item_1->itemnumber,              'Recorded an itemnumber' );
2598     is( $stat->itemtype,       $item_1->effective_itemtype,      'Recorded an itemtype' );
2599     is( $stat->borrowernumber, $patron->borrowernumber,          'Recorded a borrower number' );
2600     is( $stat->ccode,          $item_1->ccode,                   'Recorded a collection code' );
2601     is( $stat->categorycode,   $patron->categorycode,            'Recorded a categorycode' );
2602     is( $stat->location,       $item_1->location,                'Recorded a location' );
2603
2604     t::lib::Mocks::mock_userenv( { branchcode => $library->branchcode } );
2605     my $patron_2 = $builder->build_object(
2606         { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2607     my $item_2 = $builder->build_sample_item( { library => $library->branchcode } );
2608     my $issue  = AddIssue( $patron_2, $item_2->barcode );
2609     $item_2->discard_changes;
2610     ok( $item_2->onloan, "Item is checked out" );
2611
2612     my $item_3 = $builder->build_sample_item( { library => $library->branchcode } );
2613     CanBookBeIssued( $patron, $item_3->barcode );
2614     $item_3->discard_changes;
2615     is(
2616         Koha::Statistics->search( { itemnumber => $item_3->itemnumber } )->count, 1,
2617         'Single entry recorded in the stats table'
2618     );
2619
2620     my $item_4 = $builder->build_sample_item( { library => $library->branchcode } );
2621     AddIssue( $patron_2, $item_4->barcode );
2622     $item_4->discard_changes;
2623     is(
2624         Koha::Statistics->search( { itemnumber => $item_4->itemnumber } )->count, 1,
2625         'Issue should be recorded in statistics table for item 4.'
2626     );
2627     CanBookBeIssued( $patron, $item_4->barcode );
2628     $item_4->discard_changes;
2629     is(
2630         Koha::Statistics->search( { itemnumber => $item_4->itemnumber } )->count, 2,
2631         'Issue, return, and localuse should be recorded in statistics table for item 4.'
2632     );
2633
2634     # TODO There are other tests to provide here
2635 };
2636
2637
2638 subtest "Bug 27753 - Add AutoClaimReturnStatusOnCheckin" => sub {
2639     plan tests => 8;
2640
2641     t::lib::Mocks::mock_preference( 'AllowReturnToBranch',            'anywhere' );
2642     t::lib::Mocks::mock_preference( 'ClaimReturnedLostValue',         1 );
2643     t::lib::Mocks::mock_preference( 'AutoClaimReturnStatusOnCheckin', '' );
2644     t::lib::Mocks::mock_preference( 'BlockReturnOfLostItems',         1 );
2645     my $item      = $builder->build_sample_item( { library => $library2->{branchcode} } );
2646     my $patron    = $builder->build_object( { class => 'Koha::Patrons' } );
2647     my $librarian = $builder->build_object( { class => 'Koha::Patrons' } );
2648     t::lib::Mocks::mock_userenv( { patron => $librarian, branchcode => $library2->{branchcode} } );
2649     my $future   = dt_from_string()->add( days => 7 );
2650     my $checkout = $builder->build_object(
2651         {
2652             class => 'Koha::Checkouts',
2653             value => {
2654                 returndate      => undef,
2655                 renewals_count  => 0,
2656                 auto_renew      => 0,
2657                 borrowernumber  => $patron->borrowernumber,
2658                 itemnumber      => $item->itemnumber,
2659                 onsite_checkout => 0,
2660                 date_due        => $future,
2661             }
2662         }
2663     );
2664
2665     # Claim return
2666     my $claim = $checkout->claim_returned(
2667         {
2668             created_by => $patron->id,
2669             notes      => "Test note",
2670         }
2671     );
2672     is( $claim->issue_id, $checkout->id, "Return claim created for issue" );
2673     $item->discard_changes;
2674     is( $item->itemlost, 1, "Item set to lost as 1" );
2675
2676     # Return tests with feature disabled
2677     my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->{branchcode} );
2678     is(
2679         ref $messages->{ReturnClaims}, 'Koha::Checkouts::ReturnClaim',
2680         "AddReturn returns message 'ReturnClaims' containing the ReturnClaim object"
2681     );
2682     $item->discard_changes;
2683     is( $item->itemlost, 1, "Item lost status follows BlockReturnOfLostItems preference when feature is disabled" );
2684
2685     # Now test with the feature enabled
2686     t::lib::Mocks::mock_preference( 'AutoClaimReturnStatusOnCheckin', 'RETURNED_ON_CLAIM' );
2687     ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->{branchcode} );
2688     is(
2689         ref $messages->{ClaimAutoResolved}, 'Koha::Checkouts::ReturnClaim',
2690         "AddReturn returns message 'ClaimAutoResolved' containing the ReturnClaim object"
2691     );
2692     $claim->discard_changes;
2693     is( $claim->resolved_by, $librarian->borrowernumber, "Claim marked as resolved by librarian calling AddReturn when AutoClaimReturnStatusOnCheckin set" );
2694     is( $claim->resolution, 'RETURNED_ON_CLAIM', "Claim resolution set to match AutoClaimReturnStatusOnCheckin value" );
2695     $item->discard_changes;
2696     is( $item->itemlost, 1, "Item lost status follows BlockReturnOfLostItems when feature is enabled" );
2697 };
2698
2699 subtest 'MultipleReserves' => sub {
2700     plan tests => 3;
2701
2702     my $biblio = $builder->build_sample_biblio();
2703
2704     my $branch = $library2->{branchcode};
2705
2706     my $item_1 = $builder->build_sample_item(
2707         {
2708             biblionumber     => $biblio->biblionumber,
2709             library          => $branch,
2710             replacementprice => 12.00,
2711             itype            => $itemtype,
2712         }
2713     );
2714
2715     my $item_2 = $builder->build_sample_item(
2716         {
2717             biblionumber     => $biblio->biblionumber,
2718             library          => $branch,
2719             replacementprice => 12.00,
2720             itype            => $itemtype,
2721         }
2722     );
2723
2724     my $bibitems       = '';
2725     my $priority       = '1';
2726     my $resdate        = undef;
2727     my $expdate        = undef;
2728     my $notes          = '';
2729     my $checkitem      = undef;
2730     my $found          = undef;
2731
2732     # Renewing borrower
2733     my $patron = $builder->build_object(
2734         {
2735             class => 'Koha::Patrons',
2736             value => {
2737                 firstname =>  'John',
2738                 surname => 'Renewal',
2739                 categorycode => $patron_category->{categorycode},
2740                 branchcode => $branch,
2741             }
2742         }
2743     );
2744
2745     my $issue = AddIssue( $patron, $item_1->barcode);
2746     my $datedue = dt_from_string( $issue->date_due() );
2747     is (defined $issue->date_due(), 1, "item 1 checked out");
2748     my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
2749
2750     my $reserving_borrowernumber1 = $builder->build_object(
2751         {
2752             class => 'Koha::Patrons',
2753             value => {
2754                 firstname =>  'Katrin',
2755                 surname => 'Reservation',
2756                 categorycode => $patron_category->{categorycode},
2757                 branchcode => $branch,
2758             }
2759         }
2760     )->borrowernumber;
2761
2762     AddReserve(
2763         {
2764             branchcode       => $branch,
2765             borrowernumber   => $reserving_borrowernumber1,
2766             biblionumber     => $biblio->biblionumber,
2767             priority         => $priority,
2768             reservation_date => $resdate,
2769             expiration_date  => $expdate,
2770             notes            => $notes,
2771             itemnumber       => $checkitem,
2772             found            => $found,
2773         }
2774     );
2775
2776     my $reserving_borrowernumber2 = $builder->build_object(
2777         {
2778             class => 'Koha::Patrons',
2779             value => {
2780                 firstname =>  'Kirk',
2781                 surname => 'Reservation',
2782                 categorycode => $patron_category->{categorycode},
2783                 branchcode => $branch,
2784             }
2785         }
2786     )->borrowernumber;
2787
2788     AddReserve(
2789         {
2790             branchcode       => $branch,
2791             borrowernumber   => $reserving_borrowernumber2,
2792             biblionumber     => $biblio->biblionumber,
2793             priority         => $priority,
2794             reservation_date => $resdate,
2795             expiration_date  => $expdate,
2796             notes            => $notes,
2797             itemnumber       => $checkitem,
2798             found            => $found,
2799         }
2800     );
2801
2802     {
2803         my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue, 1);
2804         is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
2805     }
2806
2807     my $item_3 = $builder->build_sample_item(
2808         {
2809             biblionumber     => $biblio->biblionumber,
2810             library          => $branch,
2811             replacementprice => 12.00,
2812             itype            => $itemtype,
2813         }
2814     );
2815
2816     {
2817         my ( $renewokay, $error ) = CanBookBeRenewed($patron, $issue, 1);
2818         is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
2819     }
2820 };
2821
2822 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
2823     plan tests => 5;
2824
2825     my $library = $builder->build( { source => 'Branch' } );
2826     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2827
2828     my $biblionumber = $builder->build_sample_biblio(
2829         {
2830             branchcode => $library->{branchcode},
2831         }
2832     )->biblionumber;
2833     my $item_1 = $builder->build_sample_item(
2834         {
2835             biblionumber => $biblionumber,
2836             library      => $library->{branchcode},
2837         }
2838     );
2839
2840     my $item_2 = $builder->build_sample_item(
2841         {
2842             biblionumber => $biblionumber,
2843             library      => $library->{branchcode},
2844         }
2845     );
2846
2847     Koha::CirculationRules->set_rules(
2848         {
2849             categorycode => undef,
2850             itemtype     => undef,
2851             branchcode   => $library->{branchcode},
2852             rules        => {
2853                 reservesallowed => 25,
2854                 issuelength     => 14,
2855                 lengthunit      => 'days',
2856                 renewalsallowed => 1,
2857                 renewalperiod   => 7,
2858                 norenewalbefore => undef,
2859                 auto_renew      => 0,
2860                 fine            => .10,
2861                 chargeperiod    => 1,
2862                 maxissueqty     => 20
2863             }
2864         }
2865     );
2866
2867     my ( $error, $question, $alerts );
2868     my $issue = AddIssue( $patron, $item_1->barcode, dt_from_string->add( days => 1 ) );
2869
2870     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2871     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2872     cmp_deeply(
2873         { error => $error, alerts => $alerts },
2874         { error => {}, alerts => {} },
2875         'No error or alert should be raised'
2876     );
2877     is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
2878
2879     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2880     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2881     cmp_deeply(
2882         { error => $error, question => $question, alerts => $alerts },
2883         { error => {}, question => {}, alerts => {} },
2884         'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
2885     );
2886
2887     # Add a subscription
2888     Koha::Subscription->new({ biblionumber => $biblionumber })->store;
2889
2890     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
2891     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2892     cmp_deeply(
2893         { error => $error, question => $question, alerts => $alerts },
2894         { error => {}, question => {}, alerts => {} },
2895         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2896     );
2897
2898     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
2899     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->barcode );
2900     cmp_deeply(
2901         { error => $error, question => $question, alerts => $alerts },
2902         { error => {}, question => {}, alerts => {} },
2903         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
2904     );
2905 };
2906
2907 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
2908     plan tests => 8;
2909
2910     my $library = $builder->build( { source => 'Branch' } );
2911     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2912
2913     # Add 2 items
2914     my $biblionumber = $builder->build_sample_biblio(
2915         {
2916             branchcode => $library->{branchcode},
2917         }
2918     )->biblionumber;
2919     my $item_1 = $builder->build_sample_item(
2920         {
2921             biblionumber => $biblionumber,
2922             library      => $library->{branchcode},
2923         }
2924     );
2925     my $item_2 = $builder->build_sample_item(
2926         {
2927             biblionumber => $biblionumber,
2928             library      => $library->{branchcode},
2929         }
2930     );
2931
2932     # And the circulation rule
2933     Koha::CirculationRules->search->delete;
2934     Koha::CirculationRules->set_rules(
2935         {
2936             categorycode => undef,
2937             itemtype     => undef,
2938             branchcode   => undef,
2939             rules        => {
2940                 issuelength => 1,
2941                 firstremind => 1,        # 1 day of grace
2942                 finedays    => 2,        # 2 days of fine per day of overdue
2943                 lengthunit  => 'days',
2944             }
2945         }
2946     );
2947
2948     # Patron cannot issue item_1, they have overdues
2949     my $now = dt_from_string;
2950     my $five_days_ago = $now->clone->subtract( days => 5 );
2951     my $ten_days_ago  = $now->clone->subtract( days => 10 );
2952     AddIssue( $patron, $item_1->barcode, $five_days_ago );    # Add an overdue
2953     AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2954       ;    # Add another overdue
2955
2956     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2957     AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2958     my $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2959     is( $suspensions->count, 1, "Suspension added" );
2960     my $THE_suspension = $suspensions->next;
2961
2962     # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2963     # Same for the others
2964     my $expected_expiration = output_pref(
2965         {
2966             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2967             dateformat => 'sql',
2968             dateonly   => 1
2969         }
2970     );
2971     is( $THE_suspension->expiration, $expected_expiration, "Suspesion expiration set" );
2972
2973     AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
2974     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2975     is( $suspensions->count, 1, "Only one suspension" );
2976     $THE_suspension = $suspensions->next;
2977
2978     $expected_expiration = output_pref(
2979         {
2980             dt         => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2981             dateformat => 'sql',
2982             dateonly   => 1
2983         }
2984     );
2985     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
2986
2987     Koha::Patron::Debarments::DelUniqueDebarment(
2988         { borrowernumber => $patron->borrowernumber, type => 'SUSPENSION' } );
2989
2990     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2991     AddIssue( $patron, $item_1->barcode, $five_days_ago );    # Add an overdue
2992     AddIssue( $patron, $item_2->barcode, $ten_days_ago )
2993       ;    # Add another overdue
2994     AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
2995     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
2996     is( $suspensions->count, 1, "Only one suspension" );
2997     $THE_suspension = $suspensions->next;
2998
2999     $expected_expiration = output_pref(
3000         {
3001             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
3002             dateformat => 'sql',
3003             dateonly   => 1
3004         }
3005     );
3006     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
3007
3008     AddReturn( $item_2->barcode, $library->{branchcode}, undef, $now );
3009     $suspensions = $patron->restrictions->search( { type => 'SUSPENSION' } );
3010     is( $suspensions->count, 1, "Only one suspension" );
3011     $THE_suspension = $suspensions->next;
3012
3013     $expected_expiration = output_pref(
3014         {
3015             dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
3016             dateformat => 'sql',
3017             dateonly   => 1
3018         }
3019     );
3020     is( $THE_suspension->expiration, $expected_expiration, "Suspension expiration date updated" );
3021 };
3022
3023 subtest 'AddReturn + suspension_chargeperiod' => sub {
3024     plan tests => 29;
3025
3026     my $library = $builder->build( { source => 'Branch' } );
3027     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
3028
3029     my $biblionumber = $builder->build_sample_biblio(
3030         {
3031             branchcode => $library->{branchcode},
3032         }
3033     )->biblionumber;
3034     my $item_1 = $builder->build_sample_item(
3035         {
3036             biblionumber => $biblionumber,
3037             library      => $library->{branchcode},
3038         }
3039     );
3040
3041     # And the issuing rule
3042     Koha::CirculationRules->search->delete;
3043     Koha::CirculationRules->set_rules(
3044         {
3045             categorycode => '*',
3046             itemtype     => '*',
3047             branchcode   => '*',
3048             rules        => {
3049                 issuelength => 1,
3050                 firstremind => 0,    # 0 day of grace
3051                 finedays    => 2,    # 2 days of fine per day of overdue
3052                 suspension_chargeperiod => 1,
3053                 lengthunit              => 'days',
3054             }
3055         }
3056     );
3057
3058     my $now = dt_from_string;
3059     my $five_days_ago = $now->clone->subtract( days => 5 );
3060     # We want to charge 2 days every day, without grace
3061     # With 5 days of overdue: 5 * Z
3062     my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
3063     test_debarment_on_checkout(
3064         {
3065             item            => $item_1,
3066             library         => $library,
3067             patron          => $patron,
3068             due_date        => $five_days_ago,
3069             expiration_date => $expected_expiration,
3070         }
3071     );
3072
3073     # Same with undef firstremind
3074     Koha::CirculationRules->search->delete;
3075     Koha::CirculationRules->set_rules(
3076         {
3077             categorycode => '*',
3078             itemtype     => '*',
3079             branchcode   => '*',
3080             rules        => {
3081                 issuelength => 1,
3082                 firstremind => undef,    # 0 day of grace
3083                 finedays    => 2,    # 2 days of fine per day of overdue
3084                 suspension_chargeperiod => 1,
3085                 lengthunit              => 'days',
3086             }
3087         }
3088     );
3089     {
3090     my $now = dt_from_string;
3091     my $five_days_ago = $now->clone->subtract( days => 5 );
3092     # We want to charge 2 days every day, without grace
3093     # With 5 days of overdue: 5 * Z
3094     my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
3095     test_debarment_on_checkout(
3096         {
3097             item            => $item_1,
3098             library         => $library,
3099             patron          => $patron,
3100             due_date        => $five_days_ago,
3101             expiration_date => $expected_expiration,
3102         }
3103     );
3104     }
3105     # We want to charge 2 days every 2 days, without grace
3106     # With 5 days of overdue: (5 * 2) / 2
3107     Koha::CirculationRules->set_rule(
3108         {
3109             categorycode => undef,
3110             branchcode   => undef,
3111             itemtype     => undef,
3112             rule_name    => 'suspension_chargeperiod',
3113             rule_value   => '2',
3114         }
3115     );
3116
3117     $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
3118     test_debarment_on_checkout(
3119         {
3120             item            => $item_1,
3121             library         => $library,
3122             patron          => $patron,
3123             due_date        => $five_days_ago,
3124             expiration_date => $expected_expiration,
3125         }
3126     );
3127
3128     # We want to charge 2 days every 3 days, with 1 day of grace
3129     # With 5 days of overdue: ((5-1) / 3 ) * 2
3130     Koha::CirculationRules->set_rules(
3131         {
3132             categorycode => undef,
3133             branchcode   => undef,
3134             itemtype     => undef,
3135             rules        => {
3136                 suspension_chargeperiod => 3,
3137                 firstremind             => 1,
3138             }
3139         }
3140     );
3141     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
3142     test_debarment_on_checkout(
3143         {
3144             item            => $item_1,
3145             library         => $library,
3146             patron          => $patron,
3147             due_date        => $five_days_ago,
3148             expiration_date => $expected_expiration,
3149         }
3150     );
3151
3152     # Use finesCalendar to know if holiday must be skipped to calculate the due date
3153     # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
3154     Koha::CirculationRules->set_rules(
3155         {
3156             categorycode => undef,
3157             branchcode   => undef,
3158             itemtype     => undef,
3159             rules        => {
3160                 finedays                => 2,
3161                 suspension_chargeperiod => 1,
3162                 firstremind             => 0,
3163             }
3164         }
3165     );
3166     t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
3167     t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
3168
3169     # Adding a holiday 2 days ago
3170     my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
3171     my $two_days_ago = $now->clone->subtract( days => 2 );
3172     $calendar->insert_single_holiday(
3173         day             => $two_days_ago->day,
3174         month           => $two_days_ago->month,
3175         year            => $two_days_ago->year,
3176         title           => 'holidayTest-2d',
3177         description     => 'holidayDesc 2 days ago'
3178     );
3179     # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
3180     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
3181     test_debarment_on_checkout(
3182         {
3183             item            => $item_1,
3184             library         => $library,
3185             patron          => $patron,
3186             due_date        => $five_days_ago,
3187             expiration_date => $expected_expiration,
3188         }
3189     );
3190
3191     # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
3192     my $two_days_ahead = $now->clone->add( days => 2 );
3193     $calendar->insert_single_holiday(
3194         day             => $two_days_ahead->day,
3195         month           => $two_days_ahead->month,
3196         year            => $two_days_ahead->year,
3197         title           => 'holidayTest+2d',
3198         description     => 'holidayDesc 2 days ahead'
3199     );
3200
3201     # Same as above, but we should skip D+2
3202     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
3203     test_debarment_on_checkout(
3204         {
3205             item            => $item_1,
3206             library         => $library,
3207             patron          => $patron,
3208             due_date        => $five_days_ago,
3209             expiration_date => $expected_expiration,
3210         }
3211     );
3212
3213     # Adding another holiday, day of expiration date
3214     my $expected_expiration_dt = dt_from_string($expected_expiration);
3215     $calendar->insert_single_holiday(
3216         day             => $expected_expiration_dt->day,
3217         month           => $expected_expiration_dt->month,
3218         year            => $expected_expiration_dt->year,
3219         title           => 'holidayTest_exp',
3220         description     => 'holidayDesc on expiration date'
3221     );
3222     # Expiration date will be the day after
3223     test_debarment_on_checkout(
3224         {
3225             item            => $item_1,
3226             library         => $library,
3227             patron          => $patron,
3228             due_date        => $five_days_ago,
3229             expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
3230         }
3231     );
3232
3233     test_debarment_on_checkout(
3234         {
3235             item            => $item_1,
3236             library         => $library,
3237             patron          => $patron,
3238             return_date     => $now->clone->add(days => 5),
3239             expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
3240         }
3241     );
3242
3243     test_debarment_on_checkout(
3244         {
3245             item            => $item_1,
3246             library         => $library,
3247             patron          => $patron,
3248             due_date        => $now->clone->add(days => 1),
3249             return_date     => $now->clone->add(days => 5),
3250             expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
3251         }
3252     );
3253
3254     Koha::CirculationRules->search->delete;
3255     Koha::CirculationRules->set_rules(
3256         {
3257             categorycode => undef,
3258             itemtype     => undef,
3259             branchcode   => undef,
3260             rules => {
3261                 finedays   => 0,
3262                 lengthunit => 'days',
3263               }
3264         }
3265     );
3266
3267     Koha::Patron::Debarments::AddDebarment(
3268         {
3269             borrowernumber => $patron->borrowernumber,
3270             expiration     => '9999-12-31',
3271             type           => 'MANUAL',
3272         }
3273     );
3274
3275     AddIssue( $patron, $item_1->barcode, $now->clone->subtract( days => 1 ) );
3276     my ( undef, $message ) = AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
3277     is( $message->{WasReturned} && exists $message->{ForeverDebarred}, 1, 'Forever debarred message for Addreturn when overdue');
3278
3279     Koha::Patron::Debarments::DelUniqueDebarment(
3280         {
3281             borrowernumber => $patron->borrowernumber,
3282             type           => 'MANUAL',
3283         }
3284     );
3285     Koha::Patron::Debarments::AddDebarment(
3286         {
3287             borrowernumber => $patron->borrowernumber,
3288             expiration     => $now->clone->add( days => 10 ),
3289             type           => 'MANUAL',
3290         }
3291     );
3292
3293     AddIssue( $patron, $item_1->barcode, $now->clone->subtract( days => 1 ) );
3294     (undef, $message) = AddReturn( $item_1->barcode, $library->{branchcode}, undef, $now );
3295     is( $message->{WasReturned} && exists $message->{PrevDebarred}, 1, 'Previously debarred message for Addreturn when overdue');
3296 };
3297
3298 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
3299     plan tests => 2;
3300
3301     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
3302     my $patron1 = $builder->build_object(
3303         {
3304             class => 'Koha::Patrons',
3305             value => {
3306                 branchcode   => $library->branchcode,
3307                 categorycode => $patron_category->{categorycode}
3308             }
3309         }
3310     );
3311     my $patron2 = $builder->build_object(
3312         {
3313             class => 'Koha::Patrons',
3314             value => {
3315                 branchcode   => $library->branchcode,
3316                 categorycode => $patron_category->{categorycode}
3317             }
3318         }
3319     );
3320
3321     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
3322
3323     my $item = $builder->build_sample_item(
3324         {
3325             library      => $library->branchcode,
3326         }
3327     );
3328
3329     my ( $error, $question, $alerts );
3330     my $issue = AddIssue( $patron1, $item->barcode );
3331
3332     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
3333     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
3334     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' );
3335
3336     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
3337     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->barcode );
3338     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' );
3339
3340     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
3341 };
3342
3343
3344 subtest 'AddReturn | is_overdue' => sub {
3345     plan tests => 9;
3346
3347     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
3348     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
3349     t::lib::Mocks::mock_preference('finesMode', 'production');
3350     t::lib::Mocks::mock_preference('MaxFine', '100');
3351
3352     my $library = $builder->build( { source => 'Branch' } );
3353     my $patron  = $builder->build_object(
3354         {
3355             class => 'Koha::Patrons',
3356             value => { categorycode => $patron_category->{categorycode} }
3357         }
3358     );
3359     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
3360     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3361
3362     my $item = $builder->build_sample_item(
3363         {
3364             library      => $library->{branchcode},
3365             replacementprice => 7
3366         }
3367     );
3368
3369     Koha::CirculationRules->search->delete;
3370     Koha::CirculationRules->set_rules(
3371         {
3372             categorycode => undef,
3373             itemtype     => undef,
3374             branchcode   => undef,
3375             rules        => {
3376                 issuelength  => 6,
3377                 lengthunit   => 'days',
3378                 fine         => 1,        # Charge 1 every day of overdue
3379                 chargeperiod => 1,
3380             }
3381         }
3382     );
3383
3384     my $now   = dt_from_string;
3385     my $one_day_ago   = $now->clone->subtract( days => 1 );
3386     my $two_days_ago  = $now->clone->subtract( days => 2 );
3387     my $five_days_ago = $now->clone->subtract( days => 5 );
3388     my $ten_days_ago  = $now->clone->subtract( days => 10 );
3389
3390     # No return date specified, today will be used => 10 days overdue charged
3391     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3392     AddReturn( $item->barcode, $library->{branchcode} );
3393     is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
3394     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3395
3396     # specify return date 5 days before => no overdue charged
3397     AddIssue( $patron, $item->barcode, $five_days_ago ); # date due was 5d ago
3398     AddReturn( $item->barcode, $library->{branchcode}, undef, $ten_days_ago );
3399     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
3400     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3401
3402     # specify return date 5 days later => 5 days overdue charged
3403     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3404     AddReturn( $item->barcode, $library->{branchcode}, undef, $five_days_ago );
3405     is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
3406     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3407
3408     # specify return date 5 days later, specify exemptfine => no overdue charge
3409     AddIssue( $patron, $item->barcode, $ten_days_ago ); # date due was 10d ago
3410     AddReturn( $item->barcode, $library->{branchcode}, 1, $five_days_ago );
3411     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
3412     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3413
3414     subtest 'bug 22877 | Lost item return' => sub {
3415
3416         plan tests => 3;
3417
3418         my $issue = AddIssue( $patron, $item->barcode, $ten_days_ago );    # date due was 10d ago
3419
3420         # Fake fines cronjob on this checkout
3421         my ($fine) =
3422           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3423             $ten_days_ago, $now );
3424         UpdateFine(
3425             {
3426                 issue_id       => $issue->issue_id,
3427                 itemnumber     => $item->itemnumber,
3428                 borrowernumber => $patron->borrowernumber,
3429                 amount         => $fine,
3430                 due            => output_pref($ten_days_ago)
3431             }
3432         );
3433         is( int( $patron->account->balance() ),
3434             10, "Overdue fine of 10 days overdue" );
3435
3436         # Fake longoverdue with charge and not marking returned
3437         LostItem( $item->itemnumber, 'cronjob', 0 );
3438         is( int( $patron->account->balance() ),
3439             17, "Lost fine of 7 plus 10 days overdue" );
3440
3441         # Now we return it today
3442         AddReturn( $item->barcode, $library->{branchcode} );
3443         is( int( $patron->account->balance() ),
3444             17, "Should have a single 10 days overdue fine and lost charge" );
3445
3446         # Cleanup
3447         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3448     };
3449
3450     subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
3451
3452         plan tests => 17;
3453
3454         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
3455
3456         my $issue = AddIssue( $patron, $item->barcode, $one_day_ago );    # date due was 1d ago
3457
3458         # Fake fines cronjob on this checkout
3459         my ($fine) =
3460           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3461             $one_day_ago, $now );
3462         UpdateFine(
3463             {
3464                 issue_id       => $issue->issue_id,
3465                 itemnumber     => $item->itemnumber,
3466                 borrowernumber => $patron->borrowernumber,
3467                 amount         => $fine,
3468                 due            => output_pref($one_day_ago)
3469             }
3470         );
3471         is( int( $patron->account->balance() ),
3472             1, "Overdue fine of 1 day overdue" );
3473
3474         # Backdated return (dropbox mode example - charge should be removed)
3475         AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
3476         is( int( $patron->account->balance() ),
3477             0, "Overdue fine should be annulled" );
3478         my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3479         is( $lines->count, 0, "Overdue fine accountline has been removed");
3480
3481         $issue = AddIssue( $patron, $item->barcode, $two_days_ago );    # date due was 2d ago
3482
3483         # Fake fines cronjob on this checkout
3484         ($fine) =
3485           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3486             $two_days_ago, $now );
3487         UpdateFine(
3488             {
3489                 issue_id       => $issue->issue_id,
3490                 itemnumber     => $item->itemnumber,
3491                 borrowernumber => $patron->borrowernumber,
3492                 amount         => $fine,
3493                 due            => output_pref($one_day_ago)
3494             }
3495         );
3496         is( int( $patron->account->balance() ),
3497             2, "Overdue fine of 2 days overdue" );
3498
3499         # Payment made against fine
3500         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3501         my $debit = $lines->next;
3502         my $credit = $patron->account->add_credit(
3503             {
3504                 amount    => 2,
3505                 type      => 'PAYMENT',
3506                 interface => 'test',
3507             }
3508         );
3509         $credit->apply( { debits => [$debit] } );
3510
3511         is( int( $patron->account->balance() ),
3512             0, "Overdue fine should be paid off" );
3513         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
3514         is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
3515         my $line = $lines->next;
3516         is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
3517         is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
3518
3519         # Backdated return (dropbox mode example - charge should be removed)
3520         AddReturn( $item->barcode, $library->{branchcode}, undef, $one_day_ago );
3521         is( int( $patron->account->balance() ),
3522             -1, "Refund credit has been applied" );
3523         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
3524         is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
3525
3526         $line = $lines->next;
3527         is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
3528         is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
3529         is($line->status,'RETURNED', "Overdue fine is fixed");
3530         $line = $lines->next;
3531         is($line->amount+0,-2, "Original payment amount remains as 2");
3532         is($line->amountoutstanding+0,0, "Original payment remains applied");
3533         $line = $lines->next;
3534         is($line->amount+0,-1, "Refund amount correctly set to 1");
3535         is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
3536
3537         # Cleanup
3538         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3539     };
3540
3541     subtest 'bug 25417 | backdated return + exemptfine' => sub {
3542
3543         plan tests => 2;
3544
3545         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
3546
3547         my $issue = AddIssue( $patron, $item->barcode, $one_day_ago );    # date due was 1d ago
3548
3549         # Fake fines cronjob on this checkout
3550         my ($fine) =
3551           CalcFine( $item, $patron->categorycode, $library->{branchcode},
3552             $one_day_ago, $now );
3553         UpdateFine(
3554             {
3555                 issue_id       => $issue->issue_id,
3556                 itemnumber     => $item->itemnumber,
3557                 borrowernumber => $patron->borrowernumber,
3558                 amount         => $fine,
3559                 due            => output_pref($one_day_ago)
3560             }
3561         );
3562         is( int( $patron->account->balance() ),
3563             1, "Overdue fine of 1 day overdue" );
3564
3565         # Backdated return (dropbox mode example - charge should no longer exist)
3566         AddReturn( $item->barcode, $library->{branchcode}, 1, $one_day_ago );
3567         is( int( $patron->account->balance() ),
3568             0, "Overdue fine should be annulled" );
3569
3570         # Cleanup
3571         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
3572     };
3573
3574     subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
3575         plan tests => 7;
3576
3577         t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
3578
3579         my $due_date = dt_from_string;
3580         my $issue = AddIssue( $patron, $item->barcode, $due_date );
3581
3582         # Add fine
3583         UpdateFine(
3584             {
3585                 issue_id       => $issue->issue_id,
3586                 itemnumber     => $item->itemnumber,
3587                 borrowernumber => $patron->borrowernumber,
3588                 amount         => 0.25,
3589                 due            => output_pref($due_date)
3590             }
3591         );
3592         is( $patron->account->balance(),
3593             0.25, 'Overdue fine of $0.25 recorded' );
3594
3595         # Backdate return to exact due date and time
3596         my ( undef, $message ) =
3597           AddReturn( $item->barcode, $library->{branchcode},
3598             undef, $due_date );
3599
3600         my $accountline =
3601           Koha::Account::Lines->find( { issue_id => $issue->id } );
3602         ok( !$accountline, 'accountline removed as expected' );
3603
3604         # Re-issue
3605         $issue = AddIssue( $patron, $item->barcode, $due_date );
3606
3607         # Add fine
3608         UpdateFine(
3609             {
3610                 issue_id       => $issue->issue_id,
3611                 itemnumber     => $item->itemnumber,
3612                 borrowernumber => $patron->borrowernumber,
3613                 amount         => .25,
3614                 due            => output_pref($due_date)
3615             }
3616         );
3617         is( $patron->account->balance(),
3618             0.25, 'Overdue fine of $0.25 recorded' );
3619
3620         # Partial pay accruing fine
3621         my $lines = Koha::Account::Lines->search(
3622             {
3623                 borrowernumber => $patron->borrowernumber,
3624                 issue_id       => $issue->id
3625             }
3626         );
3627         my $debit  = $lines->next;
3628         my $credit = $patron->account->add_credit(
3629             {
3630                 amount    => .20,
3631                 type      => 'PAYMENT',
3632                 interface => 'test',
3633             }
3634         );
3635         $credit->apply( { debits => [$debit] } );
3636
3637         is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
3638
3639         # Backdate return to exact due date and time
3640         ( undef, $message ) =
3641           AddReturn( $item->barcode, $library->{branchcode},
3642             undef, $due_date );
3643
3644         $lines = Koha::Account::Lines->search(
3645             {
3646                 borrowernumber => $patron->borrowernumber,
3647                 issue_id       => $issue->id
3648             }
3649         );
3650         $accountline = $lines->next;
3651         is( $accountline->amountoutstanding + 0,
3652             0, 'Partially paid fee amount outstanding was reduced to 0' );
3653         is( $accountline->amount + 0,
3654             0, 'Partially paid fee amount was reduced to 0' );
3655         is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
3656
3657         # Cleanup
3658         Koha::Account::Lines->search(
3659             { borrowernumber => $patron->borrowernumber } )->delete;
3660     };
3661
3662     subtest 'enh 23091 | Lost item return policies' => sub {
3663         plan tests => 5;
3664
3665         my $manager = $builder->build_object({ class => "Koha::Patrons" });
3666
3667         my $branchcode_false =
3668           $builder->build( { source => 'Branch' } )->{branchcode};
3669         my $specific_rule_false = $builder->build(
3670             {
3671                 source => 'CirculationRule',
3672                 value  => {
3673                     branchcode   => $branchcode_false,
3674                     categorycode => undef,
3675                     itemtype     => undef,
3676                     rule_name    => 'lostreturn',
3677                     rule_value   => 0
3678                 }
3679             }
3680         );
3681         my $branchcode_refund =
3682           $builder->build( { source => 'Branch' } )->{branchcode};
3683         my $specific_rule_refund = $builder->build(
3684             {
3685                 source => 'CirculationRule',
3686                 value  => {
3687                     branchcode   => $branchcode_refund,
3688                     categorycode => undef,
3689                     itemtype     => undef,
3690                     rule_name    => 'lostreturn',
3691                     rule_value   => 'refund'
3692                 }
3693             }
3694         );
3695         my $branchcode_restore =
3696           $builder->build( { source => 'Branch' } )->{branchcode};
3697         my $specific_rule_restore = $builder->build(
3698             {
3699                 source => 'CirculationRule',
3700                 value  => {
3701                     branchcode   => $branchcode_restore,
3702                     categorycode => undef,
3703                     itemtype     => undef,
3704                     rule_name    => 'lostreturn',
3705                     rule_value   => 'restore'
3706                 }
3707             }
3708         );
3709         my $branchcode_charge =
3710           $builder->build( { source => 'Branch' } )->{branchcode};
3711         my $specific_rule_charge = $builder->build(
3712             {
3713                 source => 'CirculationRule',
3714                 value  => {
3715                     branchcode   => $branchcode_charge,
3716                     categorycode => undef,
3717                     itemtype     => undef,
3718                     rule_name    => 'lostreturn',
3719                     rule_value   => 'charge'
3720                 }
3721             }
3722         );
3723
3724         my $branchcode_refund_unpaid =
3725         $builder->build( { source => 'Branch' } )->{branchcode};
3726         my $specific_rule_refund_unpaid = $builder->build(
3727             {
3728                 source => 'CirculationRule',
3729                 value  => {
3730                     branchcode   => $branchcode_refund_unpaid,
3731                     categorycode => undef,
3732                     itemtype     => undef,
3733                     rule_name    => 'lostreturn',
3734                     rule_value   => 'refund_unpaid'
3735                 }
3736             }
3737         );
3738
3739         my $replacement_amount = 99.00;
3740         t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
3741         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
3742         t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
3743         t::lib::Mocks::mock_preference( 'BlockReturnOfLostItems',       0 );
3744         t::lib::Mocks::mock_preference( 'RefundLostOnReturnControl',
3745             'CheckinLibrary' );
3746         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge',
3747             undef );
3748
3749         subtest 'lostreturn | refund_unpaid' => sub {
3750             plan tests => 21;
3751
3752             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund_unpaid });
3753
3754             my $item = $builder->build_sample_item(
3755                 {
3756                     replacementprice => $replacement_amount
3757                 }
3758             );
3759
3760             # Issue the item
3761             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3762
3763             # Mark item as lost
3764             $item->itemlost(3)->store;
3765             C4::Circulation::LostItem( $item->itemnumber, 1 );
3766
3767             my $lost_fee_lines = Koha::Account::Lines->search(
3768                 {
3769                     borrowernumber  => $patron->id,
3770                     itemnumber      => $item->itemnumber,
3771                     debit_type_code => 'LOST'
3772                 }
3773             );
3774             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3775             my $lost_fee_line = $lost_fee_lines->next;
3776             is( int($lost_fee_line->amount),
3777                 $replacement_amount, 'The right LOST amount is generated' );
3778             is( int($lost_fee_line->amountoutstanding),
3779                 $replacement_amount,
3780                 'The right LOST amountoutstanding is generated' );
3781             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3782
3783             is(
3784                 int($patron->account->balance),
3785                 $replacement_amount ,
3786                 "Account balance equals the replacement amount after being charged lost fee when no payments has been made"
3787             );
3788
3789             # Return lost item without any payments having been made
3790             my ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3791
3792             $lost_fee_line->discard_changes;
3793
3794             is( int($lost_fee_line->amount), $replacement_amount, 'The LOST amount is left intact' );
3795             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero' );
3796             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set' );
3797             is(
3798                 int($patron->account->balance),
3799                 0,
3800                 'Account balance should be zero after returning item with lost fee when no payments has been made'
3801             );
3802
3803             # Create a second item
3804             $item = $builder->build_sample_item(
3805                 {
3806                     replacementprice => $replacement_amount
3807                 }
3808             );
3809
3810             # Issue the item
3811             $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3812
3813             # Mark item as lost
3814             $item->itemlost(3)->store;
3815             C4::Circulation::LostItem( $item->itemnumber, 1 );
3816
3817             $lost_fee_lines = Koha::Account::Lines->search(
3818                 {
3819                     borrowernumber  => $patron->id,
3820                     itemnumber      => $item->itemnumber,
3821                     debit_type_code => 'LOST'
3822                 }
3823             );
3824             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3825             $lost_fee_line = $lost_fee_lines->next;
3826
3827             # Make partial payment
3828             $patron->account->payin_amount({
3829                 type => 'PAYMENT',
3830                 interface => 'intranet',
3831                 payment_type => 'CASH',
3832                 user_id => $patron->borrowernumber,
3833                 amount => 39.00,
3834                 debits => [$lost_fee_line]
3835             });
3836
3837             $lost_fee_line->discard_changes;
3838
3839             is( int($lost_fee_line->amountoutstanding),
3840                 60,
3841                 'The LOST amountoutstanding is the expected amount after partial payment of lost fee'
3842             );
3843
3844             is(
3845                 int($patron->account->balance),
3846                 60,
3847                 'Account balance is the expected amount after partial payment of lost fee'
3848             );
3849
3850              # Return lost item with partial payment having been made
3851             ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3852
3853             $lost_fee_line->discard_changes;
3854
3855             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero after returning lost item with partial payment' );
3856             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set for lost item with partial payment' );
3857             is(
3858                 int($patron->account->balance),
3859                 0,
3860                 'Account balance should be zero after returning item with lost fee when partial payment has been made'
3861             );
3862
3863             # Create a third item
3864             $item = $builder->build_sample_item(
3865                 {
3866                     replacementprice => $replacement_amount
3867                 }
3868             );
3869
3870             # Issue the item
3871             $issue = C4::Circulation::AddIssue( $patron, $item->barcode );
3872
3873             # Mark item as lost
3874             $item->itemlost(3)->store;
3875             C4::Circulation::LostItem( $item->itemnumber, 1 );
3876
3877             $lost_fee_lines = Koha::Account::Lines->search(
3878                 {
3879                     borrowernumber  => $patron->id,
3880                     itemnumber      => $item->itemnumber,
3881                     debit_type_code => 'LOST'
3882                 }
3883             );
3884             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3885             $lost_fee_line = $lost_fee_lines->next;
3886
3887             # Make full payment
3888             $patron->account->payin_amount({
3889                 type => 'PAYMENT',
3890                 interface => 'intranet',
3891                 payment_type => 'CASH',
3892                 user_id => $patron->borrowernumber,
3893                 amount => $replacement_amount,
3894                 debits => [$lost_fee_line]
3895             });
3896
3897             $lost_fee_line->discard_changes;
3898
3899             is( int($lost_fee_line->amountoutstanding),
3900                 0,
3901                 'The LOST amountoutstanding is the expected amount after partial payment of lost fee'
3902             );
3903
3904             is(
3905                 int($patron->account->balance),
3906                 0,
3907                 'Account balance is the expected amount after partial payment of lost fee'
3908             );
3909
3910              # Return lost item with partial payment having been made
3911             ( $returned, $message ) = AddReturn( $item->barcode, $branchcode_refund_unpaid );
3912
3913             $lost_fee_line->discard_changes;
3914
3915             is( int($lost_fee_line->amountoutstanding) , 0, 'The LOST amountoutstanding is zero after returning lost item with full payment' );
3916             is( $lost_fee_line->status, 'FOUND', 'The FOUND status was set for lost item with partial payment' );
3917             is(
3918                 int($patron->account->balance),
3919                 0,
3920                 'Account balance should be zero after returning item with lost fee when full payment has been made'
3921             );
3922         };
3923
3924         subtest 'lostreturn | false' => sub {
3925             plan tests => 12;
3926
3927             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_false });
3928
3929             my $item = $builder->build_sample_item(
3930                 {
3931                     replacementprice => $replacement_amount
3932                 }
3933             );
3934
3935             # Issue the item
3936             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
3937
3938             # Fake fines cronjob on this checkout
3939             my ($fine) =
3940               CalcFine( $item, $patron->categorycode, $library->{branchcode},
3941                 $ten_days_ago, $now );
3942             UpdateFine(
3943                 {
3944                     issue_id       => $issue->issue_id,
3945                     itemnumber     => $item->itemnumber,
3946                     borrowernumber => $patron->borrowernumber,
3947                     amount         => $fine,
3948                     due            => output_pref($ten_days_ago)
3949                 }
3950             );
3951             my $overdue_fees = Koha::Account::Lines->search(
3952                 {
3953                     borrowernumber  => $patron->id,
3954                     itemnumber      => $item->itemnumber,
3955                     debit_type_code => 'OVERDUE'
3956                 }
3957             );
3958             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
3959             my $overdue_fee = $overdue_fees->next;
3960             is( $overdue_fee->amount + 0,
3961                 10, 'The right OVERDUE amount is generated' );
3962             is( $overdue_fee->amountoutstanding + 0,
3963                 10,
3964                 'The right OVERDUE amountoutstanding is generated' );
3965
3966             # Simulate item marked as lost
3967             $item->itemlost(3)->store;
3968             C4::Circulation::LostItem( $item->itemnumber, 1 );
3969
3970             my $lost_fee_lines = Koha::Account::Lines->search(
3971                 {
3972                     borrowernumber  => $patron->id,
3973                     itemnumber      => $item->itemnumber,
3974                     debit_type_code => 'LOST'
3975                 }
3976             );
3977             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
3978             my $lost_fee_line = $lost_fee_lines->next;
3979             is( $lost_fee_line->amount + 0,
3980                 $replacement_amount, 'The right LOST amount is generated' );
3981             is( $lost_fee_line->amountoutstanding + 0,
3982                 $replacement_amount,
3983                 'The right LOST amountoutstanding is generated' );
3984             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
3985
3986             # Return lost item
3987             my ( $returned, $message ) =
3988               AddReturn( $item->barcode, $branchcode_false, undef, $five_days_ago );
3989
3990             $overdue_fee->discard_changes;
3991             is( $overdue_fee->amount + 0,
3992                 10, 'The OVERDUE amount is left intact' );
3993             is( $overdue_fee->amountoutstanding + 0,
3994                 10,
3995                 'The OVERDUE amountoutstanding is left intact' );
3996
3997             $lost_fee_line->discard_changes;
3998             is( $lost_fee_line->amount + 0,
3999                 $replacement_amount, 'The LOST amount is left intact' );
4000             is( $lost_fee_line->amountoutstanding + 0,
4001                 $replacement_amount,
4002                 'The LOST amountoutstanding is left intact' );
4003             # FIXME: Should we set the LOST fee status to 'FOUND' regardless of whether we're refunding or not?
4004             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4005         };
4006
4007         subtest 'lostreturn | refund' => sub {
4008             plan tests => 12;
4009
4010             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_refund });
4011
4012             my $item = $builder->build_sample_item(
4013                 {
4014                     replacementprice => $replacement_amount
4015                 }
4016             );
4017
4018             # Issue the item
4019             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
4020
4021             # Fake fines cronjob on this checkout
4022             my ($fine) =
4023               CalcFine( $item, $patron->categorycode, $library->{branchcode},
4024                 $ten_days_ago, $now );
4025             UpdateFine(
4026                 {
4027                     issue_id       => $issue->issue_id,
4028                     itemnumber     => $item->itemnumber,
4029                     borrowernumber => $patron->borrowernumber,
4030                     amount         => $fine,
4031                     due            => output_pref($ten_days_ago)
4032                 }
4033             );
4034             my $overdue_fees = Koha::Account::Lines->search(
4035                 {
4036                     borrowernumber  => $patron->id,
4037                     itemnumber      => $item->itemnumber,
4038                     debit_type_code => 'OVERDUE'
4039                 }
4040             );
4041             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
4042             my $overdue_fee = $overdue_fees->next;
4043             is( $overdue_fee->amount + 0,
4044                 10, 'The right OVERDUE amount is generated' );
4045             is( $overdue_fee->amountoutstanding + 0,
4046                 10,
4047                 'The right OVERDUE amountoutstanding is generated' );
4048
4049             # Simulate item marked as lost
4050             $item->itemlost(3)->store;
4051             C4::Circulation::LostItem( $item->itemnumber, 1 );
4052
4053             my $lost_fee_lines = Koha::Account::Lines->search(
4054                 {
4055                     borrowernumber  => $patron->id,
4056                     itemnumber      => $item->itemnumber,
4057                     debit_type_code => 'LOST'
4058                 }
4059             );
4060             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
4061             my $lost_fee_line = $lost_fee_lines->next;
4062             is( $lost_fee_line->amount + 0,
4063                 $replacement_amount, 'The right LOST amount is generated' );
4064             is( $lost_fee_line->amountoutstanding + 0,
4065                 $replacement_amount,
4066                 'The right LOST amountoutstanding is generated' );
4067             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4068
4069             # Return the lost item
4070             my ( undef, $message ) =
4071               AddReturn( $item->barcode, $branchcode_refund, undef, $five_days_ago );
4072
4073             $overdue_fee->discard_changes;
4074             is( $overdue_fee->amount + 0,
4075                 10, 'The OVERDUE amount is left intact' );
4076             is( $overdue_fee->amountoutstanding + 0,
4077                 10,
4078                 'The OVERDUE amountoutstanding is left intact' );
4079
4080             $lost_fee_line->discard_changes;
4081             is( $lost_fee_line->amount + 0,
4082                 $replacement_amount, 'The LOST amount is left intact' );
4083             is( $lost_fee_line->amountoutstanding + 0,
4084                 0,
4085                 'The LOST amountoutstanding is refunded' );
4086             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
4087         };
4088
4089         subtest 'lostreturn | restore' => sub {
4090             plan tests => 13;
4091
4092             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_restore });
4093
4094             my $item = $builder->build_sample_item(
4095                 {
4096                     replacementprice => $replacement_amount
4097                 }
4098             );
4099
4100             # Issue the item
4101             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode , $ten_days_ago);
4102
4103             # Fake fines cronjob on this checkout
4104             my ($fine) =
4105               CalcFine( $item, $patron->categorycode, $library->{branchcode},
4106                 $ten_days_ago, $now );
4107             UpdateFine(
4108                 {
4109                     issue_id       => $issue->issue_id,
4110                     itemnumber     => $item->itemnumber,
4111                     borrowernumber => $patron->borrowernumber,
4112                     amount         => $fine,
4113                     due            => output_pref($ten_days_ago)
4114                 }
4115             );
4116             my $overdue_fees = Koha::Account::Lines->search(
4117                 {
4118                     borrowernumber  => $patron->id,
4119                     itemnumber      => $item->itemnumber,
4120                     debit_type_code => 'OVERDUE'
4121                 }
4122             );
4123             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
4124             my $overdue_fee = $overdue_fees->next;
4125             is( $overdue_fee->amount + 0,
4126                 10, 'The right OVERDUE amount is generated' );
4127             is( $overdue_fee->amountoutstanding + 0,
4128                 10,
4129                 'The right OVERDUE amountoutstanding is generated' );
4130
4131             # Simulate item marked as lost
4132             $item->itemlost(3)->store;
4133             C4::Circulation::LostItem( $item->itemnumber, 1 );
4134
4135             my $lost_fee_lines = Koha::Account::Lines->search(
4136                 {
4137                     borrowernumber  => $patron->id,
4138                     itemnumber      => $item->itemnumber,
4139                     debit_type_code => 'LOST'
4140                 }
4141             );
4142             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
4143             my $lost_fee_line = $lost_fee_lines->next;
4144             is( $lost_fee_line->amount + 0,
4145                 $replacement_amount, 'The right LOST amount is generated' );
4146             is( $lost_fee_line->amountoutstanding + 0,
4147                 $replacement_amount,
4148                 'The right LOST amountoutstanding is generated' );
4149             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4150
4151             # Simulate refunding overdue fees upon marking item as lost
4152             my $overdue_forgive = $patron->account->add_credit(
4153                 {
4154                     amount     => 10.00,
4155                     user_id    => $manager->borrowernumber,
4156                     library_id => $branchcode_restore,
4157                     interface  => 'test',
4158                     type       => 'FORGIVEN',
4159                     item_id    => $item->itemnumber
4160                 }
4161             );
4162             $overdue_forgive->apply( { debits => [$overdue_fee] } );
4163             $overdue_fee->discard_changes;
4164             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
4165
4166             # Do nothing
4167             my ( undef, $message ) =
4168               AddReturn( $item->barcode, $branchcode_restore, undef, $five_days_ago );
4169
4170             $overdue_fee->discard_changes;
4171             is( $overdue_fee->amount + 0,
4172                 10, 'The OVERDUE amount is left intact' );
4173             is( $overdue_fee->amountoutstanding + 0,
4174                 10,
4175                 'The OVERDUE amountoutstanding is restored' );
4176
4177             $lost_fee_line->discard_changes;
4178             is( $lost_fee_line->amount + 0,
4179                 $replacement_amount, 'The LOST amount is left intact' );
4180             is( $lost_fee_line->amountoutstanding + 0,
4181                 0,
4182                 'The LOST amountoutstanding is refunded' );
4183             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
4184         };
4185
4186         subtest 'lostreturn | charge' => sub {
4187             plan tests => 16;
4188
4189             t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $branchcode_charge });
4190
4191             my $item = $builder->build_sample_item(
4192                 {
4193                     replacementprice => $replacement_amount
4194                 }
4195             );
4196
4197             # Issue the item
4198             my $issue = C4::Circulation::AddIssue( $patron, $item->barcode, $ten_days_ago );
4199
4200             # Fake fines cronjob on this checkout
4201             my ($fine) =
4202               CalcFine( $item, $patron->categorycode, $library->{branchcode},
4203                 $ten_days_ago, $now );
4204             UpdateFine(
4205                 {
4206                     issue_id       => $issue->issue_id,
4207                     itemnumber     => $item->itemnumber,
4208                     borrowernumber => $patron->borrowernumber,
4209                     amount         => $fine,
4210                     due            => output_pref($ten_days_ago)
4211                 }
4212             );
4213             my $overdue_fees = Koha::Account::Lines->search(
4214                 {
4215                     borrowernumber  => $patron->id,
4216                     itemnumber      => $item->itemnumber,
4217                     debit_type_code => 'OVERDUE'
4218                 }
4219             );
4220             is( $overdue_fees->count, 1, 'Overdue item fee produced' );
4221             my $overdue_fee = $overdue_fees->next;
4222             is( $overdue_fee->amount + 0,
4223                 10, 'The right OVERDUE amount is generated' );
4224             is( $overdue_fee->amountoutstanding + 0,
4225                 10,
4226                 'The right OVERDUE amountoutstanding is generated' );
4227
4228             # Simulate item marked as lost
4229             $item->itemlost(3)->store;
4230             C4::Circulation::LostItem( $item->itemnumber, 1 );
4231
4232             my $lost_fee_lines = Koha::Account::Lines->search(
4233                 {
4234                     borrowernumber  => $patron->id,
4235                     itemnumber      => $item->itemnumber,
4236                     debit_type_code => 'LOST'
4237                 }
4238             );
4239             is( $lost_fee_lines->count, 1, 'Lost item fee produced' );
4240             my $lost_fee_line = $lost_fee_lines->next;
4241             is( $lost_fee_line->amount + 0,
4242                 $replacement_amount, 'The right LOST amount is generated' );
4243             is( $lost_fee_line->amountoutstanding + 0,
4244                 $replacement_amount,
4245                 'The right LOST amountoutstanding is generated' );
4246             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
4247
4248             # Simulate refunding overdue fees upon marking item as lost
4249             my $overdue_forgive = $patron->account->add_credit(
4250                 {
4251                     amount     => 10.00,
4252                     user_id    => $manager->borrowernumber,
4253                     library_id => $branchcode_charge,
4254                     interface  => 'test',
4255                     type       => 'FORGIVEN',
4256                     item_id    => $item->itemnumber
4257                 }
4258             );
4259             $overdue_forgive->apply( { debits => [$overdue_fee] } );
4260             $overdue_fee->discard_changes;
4261             is($overdue_fee->amountoutstanding + 0, 0, 'Overdue fee forgiven');
4262
4263             # Do nothing
4264             my ( undef, $message ) =
4265               AddReturn( $item->barcode, $branchcode_charge, undef, $five_days_ago );
4266
4267             $lost_fee_line->discard_changes;
4268             is( $lost_fee_line->amount + 0,
4269                 $replacement_amount, 'The LOST amount is left intact' );
4270             is( $lost_fee_line->amountoutstanding + 0,
4271                 0,
4272                 'The LOST amountoutstanding is refunded' );
4273             is( $lost_fee_line->status, 'FOUND', 'The LOST status was set to FOUND' );
4274
4275             $overdue_fees = Koha::Account::Lines->search(
4276                 {
4277                     borrowernumber  => $patron->id,
4278                     itemnumber      => $item->itemnumber,
4279                     debit_type_code => 'OVERDUE'
4280                 },
4281                 {
4282                     order_by => { '-asc' => 'accountlines_id'}
4283                 }
4284             );
4285             is( $overdue_fees->count, 2, 'A second OVERDUE fee has been added' );
4286             $overdue_fee = $overdue_fees->next;
4287             is( $overdue_fee->amount + 0,
4288                 10, 'The original OVERDUE amount is left intact' );
4289             is( $overdue_fee->amountoutstanding + 0,
4290                 0,
4291                 'The original OVERDUE amountoutstanding is left as forgiven' );
4292             $overdue_fee = $overdue_fees->next;
4293             is( $overdue_fee->amount + 0,
4294                 5, 'The new OVERDUE amount is correct for the backdated return' );
4295             is( $overdue_fee->amountoutstanding + 0,
4296                 5,
4297                 'The new OVERDUE amountoutstanding is correct for the backdated return' );
4298         };
4299     };
4300 };
4301
4302 subtest '_FixOverduesOnReturn' => sub {
4303     plan tests => 14;
4304
4305     my $manager = $builder->build_object({ class => "Koha::Patrons" });
4306     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
4307
4308     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
4309
4310     my $branchcode  = $library2->{branchcode};
4311
4312     my $item = $builder->build_sample_item(
4313         {
4314             biblionumber     => $biblio->biblionumber,
4315             library          => $branchcode,
4316             replacementprice => 99.00,
4317             itype            => $itemtype,
4318         }
4319     );
4320
4321     my $patron = $builder->build( { source => 'Borrower' } );
4322
4323     ## Start with basic call, should just close out the open fine
4324     my $accountline = Koha::Account::Line->new(
4325         {
4326             borrowernumber => $patron->{borrowernumber},
4327             debit_type_code    => 'OVERDUE',
4328             status         => 'UNRETURNED',
4329             itemnumber     => $item->itemnumber,
4330             amount => 99.00,
4331             amountoutstanding => 99.00,
4332             interface => 'test',
4333         }
4334     )->store();
4335
4336     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
4337
4338     $accountline->_result()->discard_changes();
4339
4340     is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
4341     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4342     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
4343
4344     ## Run again, with exemptfine enabled
4345     $accountline->set(
4346         {
4347             debit_type_code    => 'OVERDUE',
4348             status         => 'UNRETURNED',
4349             amountoutstanding => 99.00,
4350         }
4351     )->store();
4352
4353     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
4354
4355     $accountline->_result()->discard_changes();
4356     my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'APPLY' })->next();
4357
4358     is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
4359     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4360     is( $accountline->status, 'RETURNED', 'Open fine ( account type OVERDUE ) has been set to returned ( status RETURNED )');
4361     is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
4362     is( $offset->amount + 0, -99, "Amount of offset is correct" );
4363     my $credit = $offset->credit;
4364     is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
4365     is( $credit->amount + 0, -99, "Credit amount is set correctly" );
4366     is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
4367
4368     # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
4369     $accountline->set(
4370         {
4371             debit_type_code    => 'OVERDUE',
4372             status         => 'UNRETURNED',
4373             amountoutstanding => 0.00,
4374         }
4375     )->store();
4376     $offset->delete;
4377
4378     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
4379
4380     $accountline->_result()->discard_changes();
4381     $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'CREATE' })->next();
4382     is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
4383     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
4384     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
4385 };
4386
4387 subtest 'Set waiting flag' => sub {
4388     plan tests => 11;
4389
4390     my $library_1 = $builder->build( { source => 'Branch' } );
4391     my $patron_1  = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
4392     my $library_2 = $builder->build( { source => 'Branch' } );
4393     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
4394
4395     my $item = $builder->build_sample_item(
4396         {
4397             library      => $library_1->{branchcode},
4398         }
4399     );
4400
4401     set_userenv( $library_2 );
4402     my $reserve_id = AddReserve(
4403         {
4404             branchcode     => $library_2->{branchcode},
4405             borrowernumber => $patron_2->{borrowernumber},
4406             biblionumber   => $item->biblionumber,
4407             priority       => 1,
4408             itemnumber     => $item->itemnumber,
4409         }
4410     );
4411
4412     set_userenv( $library_1 );
4413     my $do_transfer = 1;
4414     my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
4415     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4416     my $hold = Koha::Holds->find( $reserve_id );
4417     is( $hold->found, 'T', 'Hold is in transit' );
4418
4419     my ( $status ) = CheckReserves($item);
4420     is( $status, 'Transferred', 'Hold is not waiting yet');
4421
4422     set_userenv( $library_2 );
4423     $do_transfer = 0;
4424     AddReturn( $item->barcode, $library_2->{branchcode} );
4425     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
4426     $hold = Koha::Holds->find( $reserve_id );
4427     is( $hold->found, 'W', 'Hold is waiting' );
4428     ( $status ) = CheckReserves($item);
4429     is( $status, 'Waiting', 'Now the hold is waiting');
4430
4431     #Bug 21944 - Waiting transfer checked in at branch other than pickup location
4432     set_userenv( $library_1 );
4433     (undef, my $messages, undef, undef ) = AddReturn ( $item->barcode, $library_1->{branchcode} );
4434     $hold = Koha::Holds->find( $reserve_id );
4435     is( $hold->found, undef, 'Hold is no longer marked waiting' );
4436     is( $hold->priority, 1,  "Hold is now priority one again");
4437     is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
4438     is( $hold->itemnumber, $item->itemnumber, "Hold has retained its' itemnumber");
4439     is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
4440     is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
4441     is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
4442 };
4443
4444 subtest 'Cancel transfers on lost items' => sub {
4445     plan tests => 6;
4446
4447     my $library_to = $builder->build_object( { class => 'Koha::Libraries' } );
4448     my $item   = $builder->build_sample_item();
4449     my $holdingbranch = $item->holdingbranch;
4450     # Historic transfer (datearrived is defined)
4451     my $old_transfer = $builder->build_object(
4452         {
4453             class => 'Koha::Item::Transfers',
4454             value => {
4455                 itemnumber    => $item->itemnumber,
4456                 frombranch    => $holdingbranch,
4457                 tobranch      => $library_to->branchcode,
4458                 reason        => 'Manual',
4459                 datesent      => \'NOW()',
4460                 datearrived   => \'NOW()',
4461                 datecancelled => undef,
4462                 daterequested => \'NOW()'
4463             }
4464         }
4465     );
4466     # Queued transfer (datesent is undefined)
4467     my $transfer_1 = $builder->build_object(
4468         {
4469             class => 'Koha::Item::Transfers',
4470             value => {
4471                 itemnumber    => $item->itemnumber,
4472                 frombranch    => $holdingbranch,
4473                 tobranch      => $library_to->branchcode,
4474                 reason        => 'Manual',
4475                 datesent      => undef,
4476                 datearrived   => undef,
4477                 datecancelled => undef,
4478                 daterequested => \'NOW()'
4479             }
4480         }
4481     );
4482     # In transit transfer (datesent is defined, datearrived and datecancelled are both undefined)
4483     my $transfer_2 = $builder->build_object(
4484         {
4485             class => 'Koha::Item::Transfers',
4486             value => {
4487                 itemnumber    => $item->itemnumber,
4488                 frombranch    => $holdingbranch,
4489                 tobranch      => $library_to->branchcode,
4490                 reason        => 'Manual',
4491                 datesent      => \'NOW()',
4492                 datearrived   => undef,
4493                 datecancelled => undef,
4494                 daterequested => \'NOW()'
4495             }
4496         }
4497     );
4498
4499     # Simulate item being marked as lost
4500     $item->itemlost(1)->store;
4501     LostItem( $item->itemnumber, 'test', 1 );
4502
4503     $transfer_1->discard_changes;
4504     isnt($transfer_1->datecancelled, undef, "Queud transfer was cancelled upon item lost");
4505     is($transfer_1->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
4506     $transfer_2->discard_changes;
4507     isnt($transfer_2->datecancelled, undef, "Active transfer was cancelled upon item lost");
4508     is($transfer_2->cancellation_reason, 'ItemLost', "Cancellation reason was set to 'ItemLost'");
4509     $old_transfer->discard_changes;
4510     is($old_transfer->datecancelled, undef, "Old transfers are unaffected");
4511     $item->discard_changes;
4512     is($item->holdingbranch, $holdingbranch, "Items holding branch remains unchanged");
4513 };
4514
4515 subtest 'CanBookBeIssued | is_overdue' => sub {
4516     plan tests => 3;
4517
4518     # Set a simple circ policy
4519     Koha::CirculationRules->set_rules(
4520         {
4521             categorycode => undef,
4522             branchcode   => undef,
4523             itemtype     => undef,
4524             rules        => {
4525                 maxissueqty     => 1,
4526                 reservesallowed => 25,
4527                 issuelength     => 14,
4528                 lengthunit      => 'days',
4529                 renewalsallowed => 1,
4530                 renewalperiod   => 7,
4531                 norenewalbefore => undef,
4532                 auto_renew      => 0,
4533                 fine            => .10,
4534                 chargeperiod    => 1,
4535             }
4536         }
4537     );
4538
4539     my $now   = dt_from_string()->truncate( to => 'day' );
4540     my $five_days_go = $now->clone->add( days => 5 );
4541     my $ten_days_go  = $now->clone->add( days => 10);
4542     my $library = $builder->build( { source => 'Branch' } );
4543     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
4544
4545     my $item = $builder->build_sample_item(
4546         {
4547             library      => $library->{branchcode},
4548         }
4549     );
4550
4551     my $issue = AddIssue( $patron, $item->barcode, $five_days_go ); # date due was 10d ago
4552     my $actualissue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
4553     is( output_pref({ str => $actualissue->date_due, dateonly => 1}), output_pref({ str => $five_days_go, dateonly => 1}), "First issue works");
4554     my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron, $item->barcode, $ten_days_go, undef, undef, undef);
4555     is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
4556     is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
4557 };
4558
4559 subtest 'ItemsDeniedRenewal rules are checked' => sub {
4560     plan tests => 4;
4561
4562     my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
4563     Koha::CirculationRules->set_rules(
4564         {
4565             categorycode => '*',
4566             itemtype     => '*',
4567             branchcode   => $idr_lib->branchcode,
4568             rules        => {
4569                 reservesallowed => 25,
4570                 issuelength     => 14,
4571                 lengthunit      => 'days',
4572                 renewalsallowed => 10,
4573                 renewalperiod   => 7,
4574                 norenewalbefore => undef,
4575                 auto_renew      => 0,
4576                 fine            => .10,
4577                 chargeperiod    => 1,
4578             }
4579         }
4580     );
4581
4582     my $allow_book = $builder->build_sample_item({
4583         homebranch => $idr_lib->branchcode,
4584         holdingbranch => $idr_lib->branchcode,
4585     });
4586
4587     my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
4588         branchcode => $idr_lib->branchcode,
4589         }
4590     });
4591     my $future = dt_from_string->add( days => 1 );
4592     my $issue = $builder->build_object(
4593         {
4594             class => 'Koha::Checkouts',
4595             value => {
4596                 returndate      => undef,
4597                 renewals_count  => 0,
4598                 auto_renew      => 0,
4599                 borrowernumber  => $idr_borrower->borrowernumber,
4600                 itemnumber      => $allow_book->itemnumber,
4601                 onsite_checkout => 0,
4602                 date_due        => $future,
4603             }
4604         }
4605     );
4606
4607     my $mock_item_class = Test::MockModule->new("Koha::Item");
4608     $mock_item_class->mock( 'is_denied_renewal', sub { return 1; } );
4609
4610     my ( $mayrenew, $error ) = CanBookBeRenewed( $idr_borrower, $issue );
4611     is( $mayrenew, 0, 'Renewal blocked when $item->is_denied_renewal returns true' );
4612     is( $error, 'item_denied_renewal', 'Renewal blocked when $item->is_denied_renewal returns true' );
4613
4614     $mock_item_class->unmock( 'is_denied_renewal' );
4615     $mock_item_class->mock( 'is_denied_renewal', sub { return 0; } );
4616
4617     ( $mayrenew, $error ) = CanBookBeRenewed( $idr_borrower, $issue );
4618     is( $mayrenew, 1, 'Renewal allowed when $item->is_denied_renewal returns false' );
4619     is( $error, undef, 'Renewal allowed when $item->is_denied_renewal returns false' );
4620
4621     $mock_item_class->unmock( 'is_denied_renewal' );
4622 };
4623
4624 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
4625     plan tests => 2;
4626
4627     t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
4628     my $library = $builder->build( { source => 'Branch' } );
4629     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
4630
4631     my $item = $builder->build_sample_item(
4632         {
4633             library      => $library->{branchcode},
4634         }
4635     );
4636
4637     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4638     is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4639     is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4640 };
4641
4642 subtest 'CanBookBeIssued | notforloan' => sub {
4643     plan tests => 2;
4644
4645     t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
4646
4647     my $library = $builder->build( { source => 'Branch' } );
4648     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
4649
4650     my $itemtype = $builder->build(
4651         {
4652             source => 'Itemtype',
4653             value  => { notforloan => 0, }
4654         }
4655     );
4656     my $item = $builder->build_sample_item(
4657         {
4658             library  => $library->{branchcode},
4659             itype    => $itemtype->{itemtype},
4660         }
4661     );
4662     $item->biblioitem->itemtype($itemtype->{itemtype})->store;
4663
4664     my ( $issuingimpossible, $needsconfirmation );
4665
4666
4667     subtest 'item-level_itypes = 1' => sub {
4668         plan tests => 6;
4669
4670         t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
4671         # Is for loan at item type and item level
4672         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4673         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4674         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4675
4676         # not for loan at item type level
4677         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
4678         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4679         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4680         is_deeply(
4681             $issuingimpossible,
4682             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
4683             'Item can not be issued, not for loan at item type level'
4684         );
4685
4686         # not for loan at item level
4687         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(0)->store;
4688         $item->notforloan( 1 )->store;
4689         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4690         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4691         is_deeply(
4692             $issuingimpossible,
4693             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
4694             'Item can not be issued, not for loan at item type level'
4695         );
4696     };
4697
4698     subtest 'item-level_itypes = 0' => sub {
4699         plan tests => 6;
4700
4701         t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
4702
4703         # We set another itemtype for biblioitem
4704         my $itemtype = $builder->build(
4705             {
4706                 source => 'Itemtype',
4707                 value  => { notforloan => 0, }
4708             }
4709         );
4710
4711         # for loan at item type and item level
4712         $item->notforloan(0)->store;
4713         $item->biblioitem->itemtype($itemtype->{itemtype})->store;
4714         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4715         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
4716         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
4717
4718         # not for loan at item type level
4719         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
4720         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4721         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4722         is_deeply(
4723             $issuingimpossible,
4724             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
4725             'Item can not be issued, not for loan at item type level'
4726         );
4727
4728         # not for loan at item level
4729         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(0)->store;
4730         $item->notforloan( 1 )->store;
4731         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
4732         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
4733         is_deeply(
4734             $issuingimpossible,
4735             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
4736             'Item can not be issued, not for loan at item type level'
4737         );
4738     };
4739
4740     # TODO test with AllowNotForLoanOverride = 1
4741 };
4742
4743 subtest 'CanBookBeIssued | recalls' => sub {
4744     plan tests => 3;
4745
4746     t::lib::Mocks::mock_preference("UseRecalls", 1);
4747     t::lib::Mocks::mock_preference("item-level_itypes", 1);
4748     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4749     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
4750     my $item = $builder->build_sample_item;
4751     Koha::CirculationRules->set_rules({
4752         branchcode => undef,
4753         itemtype => undef,
4754         categorycode => undef,
4755         rules => {
4756             recalls_allowed => 10,
4757         },
4758     });
4759
4760     # item-level recall
4761     my $recall = Koha::Recall->new(
4762         {   patron_id         => $patron1->borrowernumber,
4763             biblio_id         => $item->biblionumber,
4764             item_id           => $item->itemnumber,
4765             item_level        => 1,
4766             pickup_library_id => $patron1->branchcode,
4767         }
4768     )->store;
4769
4770     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
4771     is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed an item-level recall on this item" );
4772
4773     $recall->set_cancelled;
4774
4775     # biblio-level recall
4776     $recall = Koha::Recall->new(
4777         {   patron_id         => $patron1->borrowernumber,
4778             biblio_id         => $item->biblionumber,
4779             item_id           => undef,
4780             item_level        => 0,
4781             pickup_library_id => $patron1->branchcode,
4782         }
4783     )->store;
4784
4785     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
4786     is( $needsconfirmation->{RECALLED}->id, $recall->id, "Another patron has placed a biblio-level recall and this item is eligible to fill it" );
4787
4788     $recall->set_cancelled;
4789
4790     # biblio-level recall
4791     $recall = Koha::Recall->new(
4792         {   patron_id         => $patron1->borrowernumber,
4793             biblio_id         => $item->biblionumber,
4794             item_id           => undef,
4795             item_level        => 0,
4796             pickup_library_id => $patron1->branchcode,
4797         }
4798     )->store;
4799     $recall->set_waiting( { item => $item, expirationdate => dt_from_string() } );
4800
4801     my ( undef, undef, undef, $messages ) = CanBookBeIssued( $patron1, $item->barcode, undef, undef, undef, undef );
4802     is( $messages->{RECALLED}, $recall->id, "This book can be issued by this patron and they have placed a recall" );
4803
4804     $recall->set_cancelled;
4805 };
4806
4807 subtest 'CanBookBeIssued | bookings' => sub {
4808     plan tests => 4;
4809
4810     my $schema = Koha::Database->schema;
4811     $schema->storage->txn_begin;
4812
4813     my $patron1        = $builder->build_object( { class => 'Koha::Patrons' } );
4814     my $patron2        = $builder->build_object( { class => 'Koha::Patrons' } );
4815     my $pickup_library = $builder->build_object( { class => 'Koha::Libraries' } );
4816     my $item           = $builder->build_sample_item( { bookable => 1 } );
4817
4818     # item-level booking
4819     my $booking = Koha::Booking->new(
4820         {
4821             patron_id         => $patron1->borrowernumber,
4822             pickup_library_id => $pickup_library->branchcode,
4823             item_id           => $item->itemnumber,
4824             biblio_id         => $item->biblio->biblionumber,
4825             start_date        => dt_from_string()->subtract( days => 1 ),
4826             end_date          => dt_from_string()->add( days => 6 ),
4827         }
4828     )->store();
4829
4830     # Booking encompasses proposed checkout
4831     my ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4832         $patron2, $item->barcode,
4833         dt_from_string()->add( days => 5 ),
4834         undef, undef, undef
4835     );
4836     is(
4837         $issuingimpossible->{BOOKED_TO_ANOTHER}->booking_id,
4838         $booking->booking_id,
4839         "Another patron has booked this item with a start date before the proposed due date"
4840     );
4841
4842     ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4843         $patron1, $item->barcode,
4844         dt_from_string()->add( days => 5 ),
4845         undef, undef, undef
4846     );
4847     is(
4848         $alerts->{BOOKED}->booking_id,
4849         $booking->booking_id, "Booked to this user"
4850     );
4851
4852     # Booking start will clash before issue due
4853     $booking->start_date( dt_from_string()->add( days => 3 ) )->store();
4854
4855     ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4856         $patron2, $item->barcode,
4857         dt_from_string()->add( days => 5 ),
4858         undef, undef, undef
4859     );
4860     is(
4861         $needsconfirmation->{BOOKED_TO_ANOTHER}->booking_id,
4862         $booking->booking_id,
4863         "Another patron has booked this item for a period starting before the proposed due date"
4864     );
4865
4866     ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued(
4867         $patron1, $item->barcode,
4868         dt_from_string()->add( days => 5 ),
4869         undef, undef, undef
4870     );
4871     is(
4872         $needsconfirmation->{BOOKED_EARLY}->booking_id,
4873         $booking->booking_id,
4874         "Booked to this user, but they're collecting early"
4875     );
4876
4877     $schema->storage->txn_rollback;
4878 };
4879
4880 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
4881     plan tests => 1;
4882
4883     t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
4884     my $item = $builder->build_sample_item(
4885         {
4886             onloan => '2018-01-01',
4887         }
4888     );
4889
4890     AddReturn( $item->barcode, $item->homebranch );
4891     $item->discard_changes; # refresh
4892     is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
4893 };
4894
4895 subtest 'AddReturn | recalls' => sub {
4896     plan tests => 3;
4897
4898     t::lib::Mocks::mock_preference("UseRecalls", 1);
4899     t::lib::Mocks::mock_preference("item-level_itypes", 1);
4900     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4901     my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
4902     my $item1 = $builder->build_sample_item;
4903     Koha::CirculationRules->set_rules({
4904         branchcode => undef,
4905         itemtype => undef,
4906         categorycode => undef,
4907         rules => {
4908             recalls_allowed => 10,
4909         },
4910     });
4911
4912     # this item can fill a recall with pickup at this branch
4913     AddIssue( $patron1, $item1->barcode );
4914     my $recall1 = Koha::Recall->new(
4915         {   patron_id         => $patron2->borrowernumber,
4916             biblio_id         => $item1->biblionumber,
4917             item_id           => $item1->itemnumber,
4918             item_level        => 1,
4919             pickup_library_id => $item1->homebranch,
4920         }
4921     )->store;
4922     my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
4923     is( $messages->{RecallFound}->id, $recall1->id, "Recall found" );
4924     $recall1->set_cancelled;
4925
4926     # this item can fill a recall but needs transfer
4927     AddIssue( $patron1, $item1->barcode );
4928     $recall1 = Koha::Recall->new(
4929         {   patron_id         => $patron2->borrowernumber,
4930             biblio_id         => $item1->biblionumber,
4931             item_id           => $item1->itemnumber,
4932             item_level        => 1,
4933             pickup_library_id => $patron2->branchcode,
4934         }
4935     )->store;
4936     ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
4937     is( $messages->{RecallNeedsTransfer}, $item1->homebranch, "Recall requiring transfer found" );
4938     $recall1->set_cancelled;
4939
4940     # this item is already in transit, do not ask to transfer
4941     AddIssue( $patron1, $item1->barcode );
4942     $recall1 = Koha::Recall->new(
4943         {   patron_id         => $patron2->borrowernumber,
4944             biblio_id         => $item1->biblionumber,
4945             item_id           => $item1->itemnumber,
4946             item_level        => 1,
4947             pickup_library_id => $patron2->branchcode,
4948         }
4949     )->store;
4950     $recall1->start_transfer;
4951     ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $patron2->branchcode );
4952     is( $messages->{TransferredRecall}->id, $recall1->id, "In transit recall found" );
4953     $recall1->set_cancelled;
4954 };
4955
4956 subtest 'AddReturn | bundles' => sub {
4957     plan tests => 1;
4958
4959     my $schema = Koha::Database->schema;
4960     $schema->storage->txn_begin;
4961
4962     my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4963     my $host_item1 = $builder->build_sample_item;
4964     my $bundle_item1 = $builder->build_sample_item;
4965     $schema->resultset('ItemBundle')
4966       ->create(
4967         { host => $host_item1->itemnumber, item => $bundle_item1->itemnumber } );
4968
4969     my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $bundle_item1->barcode, $bundle_item1->homebranch );
4970     is($messages->{InBundle}->id, $host_item1->id, 'AddReturn returns InBundle host item when item is part of a bundle');
4971
4972     $schema->storage->txn_rollback;
4973 };
4974
4975 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
4976
4977     plan tests => 13;
4978
4979
4980     t::lib::Mocks::mock_preference('item-level_itypes', 1);
4981
4982     my $issuing_charges = 15;
4983     my $title   = 'A title';
4984     my $author  = 'Author, An';
4985     my $barcode = 'WHATARETHEODDS';
4986
4987     my $circ = Test::MockModule->new('C4::Circulation');
4988     $circ->mock(
4989         'GetIssuingCharges',
4990         sub {
4991             return $issuing_charges;
4992         }
4993     );
4994
4995     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
4996     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
4997     my $patron   = $builder->build_object({
4998         class => 'Koha::Patrons',
4999         value => { branchcode => $library->id }
5000     });
5001
5002     my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
5003     my $item_id = Koha::Item->new(
5004         {
5005             biblionumber     => $biblio->biblionumber,
5006             homebranch       => $library->id,
5007             holdingbranch    => $library->id,
5008             barcode          => $barcode,
5009             replacementprice => 23.00,
5010             itype            => $itemtype->id
5011         },
5012     )->store->itemnumber;
5013     my $item = Koha::Items->find( $item_id );
5014
5015     my $context = Test::MockModule->new('C4::Context');
5016     $context->mock( userenv => { branch => $library->id } );
5017
5018     # Check the item out
5019     AddIssue( $patron, $item->barcode );
5020
5021     throws_ok {
5022         AddRenewal(
5023             {
5024                 borrowernumber  => $patron->borrowernumber,
5025                 itemnumber      => $item->itemnumber,
5026                 branch          => $library->id,
5027                 lastreneweddate => { break => "the_renewal" }
5028             }
5029         );
5030     } 'Koha::Exceptions::Checkout::FailedRenewal', 'Exception is thrown when renewal update to issues fails';
5031
5032     t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
5033     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
5034     my %params_renewal = (
5035         timestamp => { -like => $date . "%" },
5036         module => "CIRCULATION",
5037         action => "RENEWAL",
5038     );
5039     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
5040     AddRenewal(
5041         {
5042             borrowernumber => $patron->id,
5043             itemnumber     => $item->id,
5044             branch         => $library->id
5045         }
5046     );
5047     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
5048     is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
5049
5050     my $checkouts = $patron->checkouts;
5051     # The following will fail if run on 00:00:00
5052     unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
5053
5054     my $lines = Koha::Account::Lines->search({
5055         borrowernumber => $patron->id,
5056         itemnumber     => $item->id
5057     });
5058
5059     is( $lines->count, 2 );
5060
5061     my $line = $lines->next;
5062     is( $line->debit_type_code, 'RENT',       'The issue of item with issuing charge generates an accountline of the correct type' );
5063     is( $line->branchcode,  $library->id, 'AddIssuingCharge correctly sets branchcode' );
5064     is( $line->description, '',     'AddIssue does not set a hardcoded description for the accountline' );
5065
5066     $line = $lines->next;
5067     is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
5068     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
5069     is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
5070
5071     t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
5072
5073     $context = Test::MockModule->new('C4::Context');
5074     $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
5075
5076     my $now = dt_from_string;
5077     $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
5078     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
5079     my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
5080     $sth->execute($item->id, $library->id);
5081     my ($old_stats_size) = $sth->fetchrow_array;
5082     AddRenewal(
5083         {
5084             borrowernumber => $patron->id,
5085             itemnumber     => $item->id,
5086             branch         => $library->id
5087         }
5088     );
5089     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
5090     $sth->execute($item->id, $library->id);
5091     my ($new_stats_size) = $sth->fetchrow_array;
5092     is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
5093     is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
5094
5095     AddReturn( $item->id, $library->id, undef, $date );
5096     AddIssue( $patron, $item->barcode, $now );
5097     AddRenewal(
5098         {
5099             borrowernumber => $patron->id,
5100             itemnumber     => $item->id,
5101             branch         => $library->id,
5102             skipfinecalc   => 1
5103         }
5104     );
5105     my $lines_skipped = Koha::Account::Lines->search({
5106         borrowernumber => $patron->id,
5107         itemnumber     => $item->id
5108     });
5109     is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
5110
5111 };
5112
5113 subtest 'AddRenewal() adds to renewals' => sub {
5114     plan tests => 5;
5115
5116     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
5117     my $patron   = $builder->build_object({
5118         class => 'Koha::Patrons',
5119         value => { branchcode => $library->id }
5120     });
5121
5122     my $item = $builder->build_sample_item();
5123
5124     set_userenv( $library->unblessed );
5125
5126     # Check the item out
5127     my $issue = AddIssue( $patron, $item->barcode );
5128     is(ref($issue), 'Koha::Checkout', 'Issue added');
5129
5130     # Renew item
5131     my $duedate = AddRenewal(
5132         {
5133             borrowernumber => $patron->id,
5134             itemnumber     => $item->id,
5135             branch         => $library->id,
5136             automatic      => 1
5137         }
5138     );
5139
5140     ok( $duedate, "Renewal added" );
5141
5142     my $renewals = Koha::Checkouts::Renewals->search({ checkout_id => $issue->issue_id });
5143     is($renewals->count, 1, 'One renewal added');
5144     my $THE_renewal = $renewals->next;
5145     is( $THE_renewal->renewer_id, C4::Context->userenv->{'number'}, 'Renewer recorded from context' );
5146     is( $THE_renewal->renewal_type, 'Automatic', 'AddRenewal "automatic" parameter sets renewal type to "Automatic"');
5147 };
5148
5149 subtest 'ProcessOfflinePayment() tests' => sub {
5150
5151     plan tests => 4;
5152
5153
5154     my $amount = 123;
5155
5156     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
5157     my $library = $builder->build_object({ class => 'Koha::Libraries' });
5158     my $result  = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
5159
5160     is( $result, 'Success.', 'The right string is returned' );
5161
5162     my $lines = $patron->account->lines;
5163     is( $lines->count, 1, 'line created correctly');
5164
5165     my $line = $lines->next;
5166     is( $line->amount+0, $amount * -1, 'amount picked from params' );
5167     is( $line->branchcode, $library->id, 'branchcode set correctly' );
5168
5169 };
5170
5171 subtest 'Incremented fee tests' => sub {
5172     plan tests => 19;
5173
5174     my $dt = dt_from_string();
5175     Time::Fake->offset( $dt->epoch );
5176
5177     t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
5178
5179     my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store;
5180
5181     $module->mock( 'userenv', sub { { branch => $library->id } } );
5182
5183     my $patron = $builder->build_object(
5184         {
5185             class => 'Koha::Patrons',
5186             value => { categorycode => $patron_category->{categorycode} }
5187         }
5188     );
5189
5190     my $itemtype = $builder->build_object(
5191         {
5192             class => 'Koha::ItemTypes',
5193             value => {
5194                 notforloan                   => 0,
5195                 rentalcharge                 => 0,
5196                 rentalcharge_daily           => 1,
5197                 rentalcharge_daily_calendar  => 0
5198             }
5199         }
5200     )->store;
5201
5202     my $item = $builder->build_sample_item(
5203         {
5204             library  => $library->id,
5205             itype    => $itemtype->id,
5206         }
5207     );
5208
5209     is( $itemtype->rentalcharge_daily + 0,1, 'Daily rental charge stored and retreived correctly' );
5210     is( $item->effective_itemtype, $itemtype->id, "Itemtype set correctly for item" );
5211
5212     my $now         = dt_from_string;
5213     my $dt_from     = $now->clone;
5214     my $dt_to       = $now->clone->add( days => 7 );
5215     my $dt_to_renew = $now->clone->add( days => 13 );
5216
5217     # Daily Tests
5218     my $issue =
5219       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5220     my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5221     is(
5222         $accountline->amount + 0,
5223         7,
5224         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
5225     );
5226     $accountline->delete();
5227     AddRenewal(
5228         {
5229             borrowernumber  => $patron->id,
5230             itemnumber      => $item->id,
5231             branch          => $library->id,
5232             datedue         => $dt_to_renew,
5233             lastreneweddate => $dt_to
5234         }
5235     );
5236     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5237     is(
5238         $accountline->amount + 0,
5239         6,
5240         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
5241     );
5242     $accountline->delete();
5243     $issue->delete();
5244
5245     t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
5246     $itemtype->rentalcharge_daily_calendar(1)->store();
5247     $issue =
5248       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5249     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5250     is(
5251         $accountline->amount + 0,
5252         7,
5253         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
5254     );
5255     $accountline->delete();
5256     AddRenewal(
5257         {
5258             borrowernumber  => $patron->id,
5259             itemnumber      => $item->id,
5260             branch          => $library->id,
5261             datedue         => $dt_to_renew,
5262             lastreneweddate => $dt_to
5263         }
5264     );
5265     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5266     is(
5267         $accountline->amount + 0,
5268         6,
5269         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
5270     );
5271     $accountline->delete();
5272     $issue->delete();
5273
5274     my $calendar = C4::Calendar->new( branchcode => $library->id );
5275     # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
5276     my $closed_day =
5277         ( $dt_from->day_of_week == 6 ) ? 0
5278       : ( $dt_from->day_of_week == 7 ) ? 1
5279       :                                  $dt_from->day_of_week + 1;
5280     my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
5281     $calendar->insert_week_day_holiday(
5282         weekday     => $closed_day,
5283         title       => 'Test holiday',
5284         description => 'Test holiday'
5285     );
5286     $issue =
5287       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5288     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5289     is(
5290         $accountline->amount + 0,
5291         6,
5292         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
5293     );
5294     $accountline->delete();
5295     AddRenewal(
5296         {
5297             borrowernumber  => $patron->id,
5298             itemnumber      => $item->id,
5299             branch          => $library->id,
5300             datedue         => $dt_to_renew,
5301             lastreneweddate => $dt_to
5302         }
5303     );
5304     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5305     is(
5306         $accountline->amount + 0,
5307         5,
5308         "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
5309     );
5310     $accountline->delete();
5311     $issue->delete();
5312
5313     $itemtype->rentalcharge(2)->store;
5314     is( $itemtype->rentalcharge + 0, 2, 'Rental charge updated and retreived correctly' );
5315     $issue =
5316       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5317     my $accountlines =
5318       Koha::Account::Lines->search( { itemnumber => $item->id } );
5319     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly" );
5320     $accountlines->delete();
5321     AddRenewal(
5322         {
5323             borrowernumber  => $patron->id,
5324             itemnumber      => $item->id,
5325             branch          => $library->id,
5326             datedue         => $dt_to_renew,
5327             lastreneweddate => $dt_to
5328         }
5329     );
5330     $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
5331     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly, for renewal" );
5332     $accountlines->delete();
5333     $issue->delete();
5334     $itemtype->rentalcharge(0)->store;
5335     is( $itemtype->rentalcharge + 0, 0, 'Rental charge reset and retreived correctly' );
5336
5337     # Hourly
5338     Koha::CirculationRules->set_rule(
5339         {
5340             categorycode => $patron->categorycode,
5341             itemtype     => $itemtype->id,
5342             branchcode   => $library->id,
5343             rule_name    => 'lengthunit',
5344             rule_value   => 'hours',
5345         }
5346     );
5347
5348     $itemtype->rentalcharge_hourly('0.25')->store();
5349     is( $itemtype->rentalcharge_hourly, '0.25', 'Hourly rental charge stored and retreived correctly' );
5350
5351     $dt_to       = $now->clone->add( hours => 168 );
5352     $dt_to_renew = $now->clone->add( hours => 312 );
5353
5354     $itemtype->rentalcharge_hourly_calendar(0)->store();
5355     $issue =
5356       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5357     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5358     is(
5359         $accountline->amount + 0,
5360         42,
5361         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)"
5362     );
5363     $accountline->delete();
5364     AddRenewal(
5365         {
5366             borrowernumber  => $patron->id,
5367             itemnumber      => $item->id,
5368             branch          => $library->id,
5369             datedue         => $dt_to_renew,
5370             lastreneweddate => $dt_to
5371         }
5372     );
5373     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5374     is(
5375         $accountline->amount + 0,
5376         36,
5377         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)"
5378     );
5379     $accountline->delete();
5380     $issue->delete();
5381
5382     $itemtype->rentalcharge_hourly_calendar(1)->store();
5383     $issue =
5384       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5385     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5386     is(
5387         $accountline->amount + 0,
5388         36,
5389         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)"
5390     );
5391     $accountline->delete();
5392     AddRenewal(
5393         {
5394             borrowernumber  => $patron->id,
5395             itemnumber      => $item->id,
5396             branch          => $library->id,
5397             datedue         => $dt_to_renew,
5398             lastreneweddate => $dt_to
5399         }
5400     );
5401     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5402     is(
5403         $accountline->amount + 0,
5404         30,
5405         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u"
5406     );
5407     $accountline->delete();
5408     $issue->delete();
5409
5410     $calendar->delete_holiday( weekday => $closed_day );
5411     $issue =
5412       AddIssue( $patron, $item->barcode, $dt_to, undef, $dt_from );
5413     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5414     is(
5415         $accountline->amount + 0,
5416         42,
5417         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u"
5418     );
5419     $accountline->delete();
5420     AddRenewal(
5421         {
5422             borrowernumber  => $patron->id,
5423             itemnumber      => $item->id,
5424             branch          => $library->id,
5425             datedue         => $dt_to_renew,
5426             lastreneweddate => $dt_to
5427         }
5428     );
5429     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
5430     is(
5431         $accountline->amount + 0,
5432         36,
5433         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)"
5434     );
5435     $accountline->delete();
5436     $issue->delete();
5437     Time::Fake->reset;
5438 };
5439
5440 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
5441     plan tests => 2;
5442
5443     t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
5444     t::lib::Mocks::mock_preference('item-level_itypes', 1);
5445
5446     my $library =
5447       $builder->build_object( { class => 'Koha::Libraries' } )->store;
5448     my $patron = $builder->build_object(
5449         {
5450             class => 'Koha::Patrons',
5451             value => { categorycode => $patron_category->{categorycode} }
5452         }
5453     );
5454
5455     my $itemtype = $builder->build_object(
5456         {
5457             class => 'Koha::ItemTypes',
5458             value => {
5459                 notforloan         => 0,
5460                 rentalcharge       => 0,
5461                 rentalcharge_daily => 0
5462             }
5463         }
5464     );
5465
5466     my $item = $builder->build_sample_item(
5467         {
5468             library    => $library->id,
5469             notforloan => 0,
5470             itemlost   => 0,
5471             withdrawn  => 0,
5472             itype      => $itemtype->id,
5473         }
5474     )->store;
5475
5476     my ( $issuingimpossible, $needsconfirmation );
5477     my $dt_from = dt_from_string();
5478     my $dt_due = $dt_from->clone->add( days => 3 );
5479
5480     $itemtype->rentalcharge(1)->store;
5481     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5482     is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
5483     $itemtype->rentalcharge('0')->store;
5484     $itemtype->rentalcharge_daily(1)->store;
5485     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5486     is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
5487     $itemtype->rentalcharge_daily('0')->store;
5488 };
5489
5490 subtest 'CanBookBeIssued & CircConfirmItemParts' => sub {
5491     plan tests => 1;
5492
5493     t::lib::Mocks::mock_preference('CircConfirmItemParts', 1);
5494
5495     my $patron = $builder->build_object(
5496         {
5497             class => 'Koha::Patrons',
5498             value => { categorycode => $patron_category->{categorycode} }
5499         }
5500     );
5501
5502     my $item = $builder->build_sample_item(
5503         {
5504             materials => 'includes DVD',
5505         }
5506     )->store;
5507
5508     my $dt_due = dt_from_string->add( days => 3 );
5509
5510     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
5511     is_deeply( $needsconfirmation, { ADDITIONAL_MATERIALS => 'includes DVD' }, 'Item needs confirmation of additional parts' );
5512 };
5513
5514 subtest 'Do not return on renewal (LOST charge)' => sub {
5515     plan tests => 1;
5516
5517     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
5518     my $library = $builder->build_object( { class => "Koha::Libraries" } );
5519     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
5520     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
5521
5522     my $biblio = $builder->build_sample_biblio;
5523
5524     my $item = $builder->build_sample_item(
5525         {
5526             biblionumber     => $biblio->biblionumber,
5527             library          => $library->branchcode,
5528             replacementprice => 99.00,
5529             itype            => $itemtype,
5530         }
5531     );
5532
5533     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
5534     AddIssue( $patron, $item->barcode );
5535
5536     my $accountline = Koha::Account::Line->new(
5537         {
5538             borrowernumber    => $patron->borrowernumber,
5539             debit_type_code   => 'LOST',
5540             status            => undef,
5541             itemnumber        => $item->itemnumber,
5542             amount            => 12,
5543             amountoutstanding => 12,
5544             interface         => 'something',
5545         }
5546     )->store();
5547
5548     # AddRenewal doesn't call _FixAccountForLostAndFound
5549     AddIssue( $patron, $item->barcode );
5550
5551     is( $patron->checkouts->count, 1,
5552         'Renewal should not return the item even if a LOST payment has been made earlier'
5553     );
5554 };
5555
5556 subtest 'Lost status does not change when preferences are set to 0' => sub {
5557     plan tests => 2;
5558
5559     my $library = $builder->build_object( { class => "Koha::Libraries" } );
5560     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
5561     t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
5562
5563     my $biblio = $builder->build_sample_biblio;
5564     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
5565
5566     my $item = $builder->build_sample_item(
5567         {
5568             biblionumber     => $biblio->biblionumber,
5569             library          => $library->branchcode,
5570             replacementprice => 99,
5571             itype            => $itemtype,
5572             itemlost         => 1,
5573         }
5574     );
5575
5576     my $debitCharge = Koha::Account::Line->new(
5577         {
5578             borrowernumber    => $patron->borrowernumber,
5579             debit_type_code   => 'LOST',
5580             status            => undef,
5581             itemnumber        => $item->itemnumber,
5582             amount            => 12,
5583             amountoutstanding => 12,
5584             interface         => 'something',
5585         }
5586     )->store();
5587
5588     # Test for UpdateItemLostStatusWhenPaid
5589     t::lib::Mocks::mock_preference( 'UpdateItemLostStatusWhenPaid', 0 );
5590
5591     my $paymentLine = Koha::Account::Line->new(
5592         {
5593             borrowernumber    => $patron->borrowernumber,
5594             credit_type_code  => 'CREDIT',
5595             status            => undef,
5596             itemnumber        => $item->itemnumber,
5597             amountoutstanding => 0 - 12,
5598             amount            => 0 - 12,
5599             interface         => 'something',
5600         }
5601     )->store();
5602
5603     my $offset = Koha::Account::Offset->new(
5604         {
5605             credit_id => $paymentLine->id,
5606             debit_id  => $debitCharge->id,
5607             type      => 'APPLY',
5608             amount    => 0
5609         }
5610     )->store();
5611
5612     $paymentLine->apply( { debits => [$debitCharge] } );
5613
5614     is(
5615         Koha::Items->find( $item->itemnumber )->itemlost, 1,
5616         "Payment should not change itemlost status when UpdateItemLostStatusWhenPaid is 0"
5617     );
5618
5619     # Test for UpdateItemLostStatusWhenWriteOff
5620     t::lib::Mocks::mock_preference( 'UpdateItemLostStatusWhenWriteOff', 0 );
5621
5622     my $writeOffLine = Koha::Account::Line->new(
5623         {
5624             borrowernumber    => $patron->borrowernumber,
5625             credit_type_code  => 'WRITEOFF',
5626             status            => undef,
5627             itemnumber        => $item->itemnumber,
5628             amountoutstanding => 0 - 12,
5629             amount            => 0 - 12,
5630             interface         => 'something',
5631         }
5632     )->store();
5633
5634     $offset = Koha::Account::Offset->new(
5635         {
5636             credit_id => $writeOffLine->id,
5637             debit_id  => $debitCharge->id,
5638             type      => 'APPLY',
5639             amount    => 0
5640         }
5641     )->store();
5642
5643     $writeOffLine->apply( { debits => [$debitCharge] } );
5644
5645     is(
5646         Koha::Items->find( $item->itemnumber )->itemlost, 1,
5647         "Write-off should not change itemlost status when UpdateItemLostStatusWhenWriteOff is 0"
5648     );
5649 };
5650
5651 # Test for UpdateItemLostStatusWhenPaid
5652 subtest 'Update lost item to authorized value on payment of balance' => sub {
5653     plan tests => 5;
5654
5655     my $library = $builder->build_object( { class => "Koha::Libraries" } );
5656     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
5657     t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
5658
5659     my $biblio = $builder->build_sample_biblio;
5660     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
5661
5662     for my $status ( 2 .. 6 ) {
5663         t::lib::Mocks::mock_preference( 'UpdateItemLostStatusWhenPaid', $status );
5664
5665         my $item = $builder->build_sample_item(
5666             {
5667                 biblionumber     => $biblio->biblionumber,
5668                 library          => $library->branchcode,
5669                 replacementprice => 99,
5670                 itype            => $itemtype,
5671                 itemlost         => 1,
5672             }
5673         );
5674
5675         my $debitCharge = Koha::Account::Line->new(
5676             {
5677                 borrowernumber    => $patron->borrowernumber,
5678                 debit_type_code   => 'LOST',
5679                 status            => undef,
5680                 itemnumber        => $item->itemnumber,
5681                 amount            => 12,
5682                 amountoutstanding => 12,
5683                 interface         => 'something',
5684             }
5685         )->store();
5686
5687         my $paymentLine = Koha::Account::Line->new(
5688             {
5689                 borrowernumber    => $patron->borrowernumber,
5690                 credit_type_code  => 'CREDIT',
5691                 status            => undef,
5692                 itemnumber        => $item->itemnumber,
5693                 amountoutstanding => 0 - 12,
5694                 amount            => 0 - 12,
5695                 interface         => 'something',
5696             }
5697         )->store();
5698
5699         my $offset = Koha::Account::Offset->new(
5700             {
5701                 credit_id => $paymentLine->id,
5702                 debit_id  => $debitCharge->id,
5703                 type      => 'APPLY',
5704                 amount    => 0
5705             }
5706         )->store();
5707
5708         $paymentLine->apply( { debits => [$debitCharge] } );
5709
5710         is(
5711             Koha::Items->find( $item->itemnumber )->itemlost, $status,
5712             "Payment should set itemlost to status $status"
5713         );
5714     }
5715 };
5716
5717 # Test for UpdateItemLostStatusWhenWriteOff
5718 subtest 'Update lost item to authorized value on write-off of balance' => sub {
5719     plan tests => 5;
5720
5721     my $library = $builder->build_object( { class => "Koha::Libraries" } );
5722     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
5723     t::lib::Mocks::mock_userenv( { patron => $manager, branchcode => $manager->branchcode } );
5724
5725     my $biblio = $builder->build_sample_biblio;
5726     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
5727
5728     for my $status ( 2 .. 6 ) {
5729         t::lib::Mocks::mock_preference( 'UpdateItemLostStatusWhenWriteOff', $status );
5730
5731         my $item = $builder->build_sample_item(
5732             {
5733                 biblionumber     => $biblio->biblionumber,
5734                 library          => $library->branchcode,
5735                 replacementprice => 99,
5736                 itype            => $itemtype,
5737                 itemlost         => 1,
5738             }
5739         );
5740
5741         my $debitCharge = Koha::Account::Line->new(
5742             {
5743                 borrowernumber    => $patron->borrowernumber,
5744                 debit_type_code   => 'LOST',
5745                 status            => undef,
5746                 itemnumber        => $item->itemnumber,
5747                 amount            => 12,
5748                 amountoutstanding => 12,
5749                 interface         => 'something',
5750             }
5751         )->store();
5752
5753         my $writeOffLine = Koha::Account::Line->new(
5754             {
5755                 borrowernumber    => $patron->borrowernumber,
5756                 credit_type_code  => 'WRITEOFF',
5757                 status            => undef,
5758                 itemnumber        => $item->itemnumber,
5759                 amountoutstanding => 0 - 12,
5760                 amount            => 0 - 12,
5761                 interface         => 'something',
5762             }
5763         )->store();
5764
5765         my $offset = Koha::Account::Offset->new(
5766             {
5767                 credit_id => $writeOffLine->id,
5768                 debit_id  => $debitCharge->id,
5769                 type      => 'APPLY',
5770                 amount    => 0
5771             }
5772         )->store();
5773
5774         $writeOffLine->apply( { debits => [$debitCharge] } );
5775
5776         is(
5777             Koha::Items->find( $item->itemnumber )->itemlost, $status,
5778             "Write-off should set itemlost to status $status"
5779         );
5780     }
5781 };
5782
5783 subtest 'Filling a hold should cancel existing transfer' => sub {
5784     plan tests => 4;
5785
5786     t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
5787
5788     my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
5789     my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
5790     my $patron = $builder->build_object(
5791         {
5792             class => 'Koha::Patrons',
5793             value => {
5794                 categorycode => $patron_category->{categorycode},
5795                 branchcode => $libraryA->branchcode,
5796             }
5797         }
5798     );
5799
5800     my $item = $builder->build_sample_item({
5801         homebranch => $libraryB->branchcode,
5802     });
5803
5804     my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
5805     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
5806     AddReserve({
5807         branchcode     => $libraryA->branchcode,
5808         borrowernumber => $patron->borrowernumber,
5809         biblionumber   => $item->biblionumber,
5810         itemnumber     => $item->itemnumber
5811     });
5812     my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
5813     is( $reserves->count, 1, "Reserve is placed");
5814     ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
5815     my $reserve = $reserves->next;
5816     ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
5817     $reserve->discard_changes;
5818     ok( $reserve->found eq 'W', "Reserve is marked waiting" );
5819     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
5820 };
5821
5822 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddReturn' => sub {
5823
5824     plan tests => 4;
5825
5826     t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
5827     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
5828     my $patron  = $builder->build_object(
5829         {
5830             class => 'Koha::Patrons',
5831             value => { categorycode => $patron_category->{categorycode} }
5832         }
5833     );
5834
5835     my $biblionumber = $builder->build_sample_biblio(
5836         {
5837             branchcode => $library->branchcode,
5838         }
5839     )->biblionumber;
5840
5841     # And the circulation rule
5842     Koha::CirculationRules->search->delete;
5843     Koha::CirculationRules->set_rules(
5844         {
5845             categorycode => undef,
5846             itemtype     => undef,
5847             branchcode   => undef,
5848             rules        => {
5849                 issuelength => 14,
5850                 lengthunit  => 'days',
5851             }
5852         }
5853     );
5854     $builder->build(
5855         {
5856             source => 'CirculationRule',
5857             value  => {
5858                 branchcode   => undef,
5859                 categorycode => undef,
5860                 itemtype     => undef,
5861                 rule_name    => 'lostreturn',
5862                 rule_value   => 'refund'
5863             }
5864         }
5865     );
5866
5867     subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
5868         plan tests => 3;
5869
5870         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5871         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
5872
5873         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5874
5875         my $item = $builder->build_sample_item(
5876             {
5877                 biblionumber     => $biblionumber,
5878                 library          => $library->branchcode,
5879                 replacementprice => '42',
5880             }
5881         );
5882         my $issue = AddIssue( $patron, $item->barcode );
5883         LostItem( $item->itemnumber, 'cli', 0 );
5884         $item->_result->itemlost(1);
5885         $item->_result->itemlost_on( $lost_on );
5886         $item->_result->update();
5887
5888         my $a = Koha::Account::Lines->search(
5889             {
5890                 itemnumber     => $item->id,
5891                 borrowernumber => $patron->borrowernumber
5892             }
5893         )->next;
5894         ok( $a, "Found accountline for lost fee" );
5895         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5896         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5897         $a = $a->get_from_storage;
5898         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5899         $a->delete;
5900     };
5901
5902     subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
5903         plan tests => 3;
5904
5905         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5906         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5907
5908         my $lost_on = dt_from_string->subtract( days => 6 )->date;
5909
5910         my $item = $builder->build_sample_item(
5911             {
5912                 biblionumber     => $biblionumber,
5913                 library          => $library->branchcode,
5914                 replacementprice => '42',
5915             }
5916         );
5917         my $issue = AddIssue( $patron, $item->barcode );
5918         LostItem( $item->itemnumber, 'cli', 0 );
5919         $item->_result->itemlost(1);
5920         $item->_result->itemlost_on( $lost_on );
5921         $item->_result->update();
5922
5923         my $a = Koha::Account::Lines->search(
5924             {
5925                 itemnumber     => $item->id,
5926                 borrowernumber => $patron->borrowernumber
5927             }
5928         )->next;
5929         ok( $a, "Found accountline for lost fee" );
5930         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5931         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5932         $a = $a->get_from_storage;
5933         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
5934         $a->delete;
5935     };
5936
5937     subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
5938         plan tests => 3;
5939
5940         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5941         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5942
5943         my $lost_on = dt_from_string->subtract( days => 7 )->date;
5944
5945         my $item = $builder->build_sample_item(
5946             {
5947                 biblionumber     => $biblionumber,
5948                 library          => $library->branchcode,
5949                 replacementprice => '42',
5950             }
5951         );
5952         my $issue = AddIssue( $patron, $item->barcode );
5953         LostItem( $item->itemnumber, 'cli', 0 );
5954         $item->_result->itemlost(1);
5955         $item->_result->itemlost_on( $lost_on );
5956         $item->_result->update();
5957
5958         my $a = Koha::Account::Lines->search(
5959             {
5960                 itemnumber     => $item->id,
5961                 borrowernumber => $patron->borrowernumber
5962             }
5963         )->next;
5964         ok( $a, "Found accountline for lost fee" );
5965         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
5966         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
5967         $a = $a->get_from_storage;
5968         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
5969         $a->delete;
5970     };
5971
5972     subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
5973         plan tests => 3;
5974
5975         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
5976         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
5977
5978         my $lost_on = dt_from_string->subtract( days => 8 )->date;
5979
5980         my $item = $builder->build_sample_item(
5981             {
5982                 biblionumber     => $biblionumber,
5983                 library          => $library->branchcode,
5984                 replacementprice => '42',
5985             }
5986         );
5987         my $issue = AddIssue( $patron, $item->barcode );
5988         LostItem( $item->itemnumber, 'cli', 0 );
5989         $item->_result->itemlost(1);
5990         $item->_result->itemlost_on( $lost_on );
5991         $item->_result->update();
5992
5993         my $a = Koha::Account::Lines->search(
5994             {
5995                 itemnumber     => $item->id,
5996                 borrowernumber => $patron->borrowernumber
5997             }
5998         );
5999         $a = $a->next;
6000         ok( $a, "Found accountline for lost fee" );
6001         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
6002         my ( $doreturn, $messages ) = AddReturn( $item->barcode, $library->branchcode, undef, dt_from_string );
6003         $a = $a->get_from_storage;
6004         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
6005         $a->delete;
6006     };
6007 };
6008
6009 subtest 'Tests for NoRefundOnLostReturnedItemsAge with AddIssue' => sub {
6010
6011     plan tests => 4;
6012
6013     t::lib::Mocks::mock_preference('BlockReturnOfLostItems', 0);
6014     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
6015     my $patron  = $builder->build_object(
6016         {
6017             class => 'Koha::Patrons',
6018             value => { categorycode => $patron_category->{categorycode} }
6019         }
6020     );
6021     my $patron2  = $builder->build_object(
6022         {
6023             class => 'Koha::Patrons',
6024             value => { categorycode => $patron_category->{categorycode} }
6025         }
6026     );
6027
6028     my $biblionumber = $builder->build_sample_biblio(
6029         {
6030             branchcode => $library->branchcode,
6031         }
6032     )->biblionumber;
6033
6034     # And the circulation rule
6035     Koha::CirculationRules->search->delete;
6036     Koha::CirculationRules->set_rules(
6037         {
6038             categorycode => undef,
6039             itemtype     => undef,
6040             branchcode   => undef,
6041             rules        => {
6042                 issuelength => 14,
6043                 lengthunit  => 'days',
6044             }
6045         }
6046     );
6047     $builder->build(
6048         {
6049             source => 'CirculationRule',
6050             value  => {
6051                 branchcode   => undef,
6052                 categorycode => undef,
6053                 itemtype     => undef,
6054                 rule_name    => 'lostreturn',
6055                 rule_value   => 'refund'
6056             }
6057         }
6058     );
6059
6060     subtest 'NoRefundOnLostReturnedItemsAge = undef' => sub {
6061         plan tests => 3;
6062
6063         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
6064         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', undef );
6065
6066         my $lost_on = dt_from_string->subtract( days => 7 )->date;
6067
6068         my $item = $builder->build_sample_item(
6069             {
6070                 biblionumber     => $biblionumber,
6071                 library          => $library->branchcode,
6072                 replacementprice => '42',
6073             }
6074         );
6075         my $issue = AddIssue( $patron, $item->barcode );
6076         LostItem( $item->itemnumber, 'cli', 0 );
6077         $item->_result->itemlost(1);
6078         $item->_result->itemlost_on( $lost_on );
6079         $item->_result->update();
6080
6081         my $a = Koha::Account::Lines->search(
6082             {
6083                 itemnumber     => $item->id,
6084                 borrowernumber => $patron->borrowernumber
6085             }
6086         )->next;
6087         ok( $a, "Found accountline for lost fee" );
6088         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
6089         $issue = AddIssue( $patron2, $item->barcode );
6090         $a = $a->get_from_storage;
6091         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
6092         $a->delete;
6093         $issue->delete;
6094     };
6095
6096     subtest 'NoRefundOnLostReturnedItemsAge > length of days item has been lost' => sub {
6097         plan tests => 3;
6098
6099         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
6100         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
6101
6102         my $lost_on = dt_from_string->subtract( days => 6 )->date;
6103
6104         my $item = $builder->build_sample_item(
6105             {
6106                 biblionumber     => $biblionumber,
6107                 library          => $library->branchcode,
6108                 replacementprice => '42',
6109             }
6110         );
6111         my $issue = AddIssue( $patron, $item->barcode );
6112         LostItem( $item->itemnumber, 'cli', 0 );
6113         $item->_result->itemlost(1);
6114         $item->_result->itemlost_on( $lost_on );
6115         $item->_result->update();
6116
6117         my $a = Koha::Account::Lines->search(
6118             {
6119                 itemnumber     => $item->id,
6120                 borrowernumber => $patron->borrowernumber
6121             }
6122         )->next;
6123         ok( $a, "Found accountline for lost fee" );
6124         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
6125         $issue = AddIssue( $patron2, $item->barcode );
6126         $a = $a->get_from_storage;
6127         is( $a->amountoutstanding + 0, 0, "Lost fee was refunded" );
6128         $a->delete;
6129     };
6130
6131     subtest 'NoRefundOnLostReturnedItemsAge = length of days item has been lost' => sub {
6132         plan tests => 3;
6133
6134         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
6135         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
6136
6137         my $lost_on = dt_from_string->subtract( days => 7 )->date;
6138
6139         my $item = $builder->build_sample_item(
6140             {
6141                 biblionumber     => $biblionumber,
6142                 library          => $library->branchcode,
6143                 replacementprice => '42',
6144             }
6145         );
6146         my $issue = AddIssue( $patron, $item->barcode );
6147         LostItem( $item->itemnumber, 'cli', 0 );
6148         $item->_result->itemlost(1);
6149         $item->_result->itemlost_on( $lost_on );
6150         $item->_result->update();
6151
6152         my $a = Koha::Account::Lines->search(
6153             {
6154                 itemnumber     => $item->id,
6155                 borrowernumber => $patron->borrowernumber
6156             }
6157         )->next;
6158         ok( $a, "Found accountline for lost fee" );
6159         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
6160         $issue = AddIssue( $patron2, $item->barcode );
6161         $a = $a->get_from_storage;
6162         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
6163         $a->delete;
6164     };
6165
6166     subtest 'NoRefundOnLostReturnedItemsAge < length of days item has been lost' => sub {
6167         plan tests => 3;
6168
6169         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee',   1 );
6170         t::lib::Mocks::mock_preference( 'NoRefundOnLostReturnedItemsAge', 7 );
6171
6172         my $lost_on = dt_from_string->subtract( days => 8 )->date;
6173
6174         my $item = $builder->build_sample_item(
6175             {
6176                 biblionumber     => $biblionumber,
6177                 library          => $library->branchcode,
6178                 replacementprice => '42',
6179             }
6180         );
6181         my $issue = AddIssue( $patron, $item->barcode );
6182         LostItem( $item->itemnumber, 'cli', 0 );
6183         $item->_result->itemlost(1);
6184         $item->_result->itemlost_on( $lost_on );
6185         $item->_result->update();
6186
6187         my $a = Koha::Account::Lines->search(
6188             {
6189                 itemnumber     => $item->id,
6190                 borrowernumber => $patron->borrowernumber
6191             }
6192         );
6193         $a = $a->next;
6194         ok( $a, "Found accountline for lost fee" );
6195         is( $a->amountoutstanding + 0, 42, "Lost fee charged correctly" );
6196         $issue = AddIssue( $patron2, $item->barcode );
6197         $a = $a->get_from_storage;
6198         is( $a->amountoutstanding + 0, 42, "Lost fee was not refunded" );
6199         $a->delete;
6200     };
6201 };
6202
6203 subtest 'transferbook tests' => sub {
6204     plan tests => 9;
6205
6206     throws_ok
6207     { C4::Circulation::transferbook({}); }
6208     'Koha::Exceptions::MissingParameter',
6209     'Koha::Patron->store raises an exception on missing params';
6210
6211     throws_ok
6212     { C4::Circulation::transferbook({to_branch=>'anything'}); }
6213     'Koha::Exceptions::MissingParameter',
6214     'Koha::Patron->store raises an exception on missing params';
6215
6216     throws_ok
6217     { C4::Circulation::transferbook({from_branch=>'anything'}); }
6218     'Koha::Exceptions::MissingParameter',
6219     'Koha::Patron->store raises an exception on missing params';
6220
6221     my ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here'});
6222     is( $doreturn, 0, "No return without barcode");
6223     ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
6224     is( $messages->{BadBarcode}, undef, "No barcode passed means undef BadBarcode" );
6225
6226     ($doreturn,$messages) = C4::Circulation::transferbook({to_branch=>'there',from_branch=>'here',barcode=>'BadBarcode'});
6227     is( $doreturn, 0, "No return without barcode");
6228     ok( exists $messages->{BadBarcode}, "We get a BadBarcode message if no barcode passed");
6229     is( $messages->{BadBarcode}, 'BadBarcode', "No barcode passed means undef BadBarcode" );
6230
6231 };
6232
6233 subtest 'Checkout should correctly terminate a transfer' => sub {
6234     plan tests => 7;
6235
6236     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
6237     my $patron_1 = $builder->build_object(
6238         {
6239             class => 'Koha::Patrons',
6240             value => { branchcode => $library_1->branchcode }
6241         }
6242     );
6243     my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
6244     my $patron_2 = $builder->build_object(
6245         {
6246             class => 'Koha::Patrons',
6247             value => { branchcode => $library_2->branchcode }
6248         }
6249     );
6250
6251     my $item = $builder->build_sample_item(
6252         {
6253             library => $library_1->branchcode,
6254         }
6255     );
6256
6257     t::lib::Mocks::mock_userenv( { branchcode => $library_1->branchcode } );
6258     my $reserve_id = AddReserve(
6259         {
6260             branchcode     => $library_2->branchcode,
6261             borrowernumber => $patron_2->borrowernumber,
6262             biblionumber   => $item->biblionumber,
6263             itemnumber     => $item->itemnumber,
6264             priority       => 1,
6265         }
6266     );
6267
6268     my $do_transfer = 1;
6269     ModItemTransfer( $item->itemnumber, $library_1->branchcode,
6270         $library_2->branchcode, 'Reserve' );
6271     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
6272     my $hold = Koha::Holds->find($reserve_id);
6273     is( $hold->found, 'T', 'Hold is in transit' );
6274     my $transfer = $item->get_transfer;
6275     is( $transfer->frombranch, $library_1->branchcode );
6276     is( $transfer->tobranch,   $library_2->branchcode );
6277     is( $transfer->reason,     'Reserve' );
6278
6279     t::lib::Mocks::mock_userenv( { branchcode => $library_2->branchcode } );
6280     AddIssue( $patron_1, $item->barcode );
6281     $transfer = $transfer->get_from_storage;
6282     isnt( $transfer->datearrived, undef );
6283     $hold = $hold->get_from_storage;
6284     is( $hold->found, undef, 'Hold is not waiting or transit' );
6285     is( $hold->priority, 1, );
6286 };
6287
6288 subtest 'AddIssue records staff who checked out item if appropriate' => sub  {
6289     plan tests => 2;
6290
6291     $module->mock( 'userenv', sub { { branch => $library->{id} } } );
6292
6293     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
6294     my $patron = $builder->build_object(
6295         {
6296             class => 'Koha::Patrons',
6297             value => { categorycode => $patron_category->{categorycode} }
6298         }
6299     );
6300     my $issuer = $builder->build_object(
6301         {
6302             class => 'Koha::Patrons',
6303             value => { categorycode => $patron_category->{categorycode} }
6304         }
6305     );
6306     my $item_1 = $builder->build_sample_item(
6307         {
6308             library  => $library->{branchcode}
6309         }
6310     );
6311
6312     my $item_2 = $builder->build_sample_item(
6313         {
6314             library  => $library->branchcode
6315         }
6316     );
6317
6318     $module->mock( 'userenv', sub { { branch => $library->id, number => $issuer->borrowernumber } } );
6319
6320     my $dt_from = dt_from_string();
6321     my $dt_to   = dt_from_string()->add( days => 7 );
6322
6323     my $issue_1 = AddIssue( $patron, $item_1->barcode, $dt_to, undef, $dt_from );
6324
6325     is( $issue_1->issuer, undef, "Staff who checked out the item not recorded when RecordStaffUserOnCheckout turned off" );
6326
6327     t::lib::Mocks::mock_preference('RecordStaffUserOnCheckout', 1);
6328
6329     my $issue_2 =
6330       AddIssue( $patron, $item_2->barcode, $dt_to, undef, $dt_from );
6331
6332     is( $issue_2->issuer->borrowernumber, $issuer->borrowernumber, "Staff who checked out the item recorded when RecordStaffUserOnCheckout turned on" );
6333 };
6334
6335 subtest "Item's onloan value should be set if checked out item is checked out to a different patron" => sub {
6336     plan tests => 2;
6337
6338     my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
6339     my $patron_1 = $builder->build_object(
6340         {
6341             class => 'Koha::Patrons',
6342             value => { branchcode => $library_1->branchcode }
6343         }
6344     );
6345     my $patron_2 = $builder->build_object(
6346         {
6347             class => 'Koha::Patrons',
6348             value => { branchcode => $library_1->branchcode }
6349         }
6350     );
6351
6352     my $item = $builder->build_sample_item(
6353         {
6354             library => $library_1->branchcode,
6355         }
6356     );
6357
6358     AddIssue( $patron_1, $item->barcode );
6359     ok( $item->get_from_storage->onloan, "Item's onloan column is set after initial checkout" );
6360     AddIssue( $patron_2, $item->barcode );
6361     ok( $item->get_from_storage->onloan, "Item's onloan column is set after second checkout" );
6362 };
6363
6364 subtest "updateWrongTransfer tests" => sub {
6365     plan tests => 5;
6366
6367     my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
6368     my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
6369     my $library3 = $builder->build_object( { class => 'Koha::Libraries' } );
6370     my $item     = $builder->build_sample_item(
6371         {
6372             homebranch    => $library1->branchcode,
6373             holdingbranch => $library2->branchcode,
6374             datelastseen  => undef
6375         }
6376     );
6377
6378     my $transfer = $builder->build_object(
6379         {
6380             class => 'Koha::Item::Transfers',
6381             value => {
6382                 itemnumber    => $item->itemnumber,
6383                 frombranch    => $library2->branchcode,
6384                 tobranch      => $library1->branchcode,
6385                 daterequested => dt_from_string,
6386                 datesent      => dt_from_string,
6387                 datecancelled => undef,
6388                 datearrived   => undef,
6389                 reason        => 'Manual'
6390             }
6391         }
6392     );
6393     is( ref($transfer), 'Koha::Item::Transfer', 'Mock transfer added' );
6394
6395     my $new_transfer = C4::Circulation::updateWrongTransfer($item->itemnumber, $library1->branchcode);
6396     is(ref($new_transfer), 'Koha::Item::Transfer', "updateWrongTransfer returns a 'Koha::Item::Transfer' object");
6397     ok( !$new_transfer->in_transit, "New transfer is NOT created as in transit (or cancelled)");
6398
6399     my $original_transfer = $transfer->get_from_storage;
6400     ok( defined($original_transfer->datecancelled), "Original transfer was cancelled");
6401     is( $original_transfer->cancellation_reason, 'WrongTransfer', "Original transfer cancellation reason is 'WrongTransfer'");
6402 };
6403
6404 subtest "SendCirculationAlert" => sub {
6405     plan tests => 3;
6406
6407     # When you would unsuspectingly call this unit test (with perl, not prove), you will be bitten by LOCK.
6408     # LOCK will commit changes and ruin your data
6409     # In order to prevent that, we will add KOHA_TESTING to $ENV; see further Circulation.pm
6410     $ENV{KOHA_TESTING} = 1;
6411
6412     # Setup branch, borrowr, and notice
6413     my $library = $builder->build_object({ class => 'Koha::Libraries' });
6414     set_userenv( $library->unblessed);
6415     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
6416     C4::Members::Messaging::SetMessagingPreference({
6417         borrowernumber => $patron->id,
6418         message_transport_types => ['sms'],
6419         message_attribute_id => 5
6420     });
6421     my $item = $builder->build_sample_item();
6422     my $checkin_notice = $builder->build_object({
6423         class => 'Koha::Notice::Templates',
6424         value =>{
6425             module => 'circulation',
6426             code => 'CHECKIN',
6427             branchcode => $library->branchcode,
6428             name => 'Test Checkin',
6429             is_html => 0,
6430             content => "Checkins:\n----\n[% biblio.title %]-[% old_checkout.issue_id %]\n----Thank you.",
6431             message_transport_type => 'sms',
6432             lang => 'default'
6433         }
6434     })->store;
6435
6436     # Checkout an item, mark it returned, generate a notice
6437     my $issue_1 = AddIssue( $patron, $item->barcode);
6438     MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
6439     C4::Circulation::SendCirculationAlert({
6440         type => 'CHECKIN',
6441         item => $item->unblessed,
6442         borrower => $patron->unblessed,
6443         branch => $library->branchcode,
6444         issue => $issue_1
6445     });
6446     my $notice = Koha::Notice::Messages->find({ borrowernumber => $patron->id, letter_code => 'CHECKIN' });
6447     is($notice->content,"Checkins:\n".$item->biblio->title."-".$issue_1->id."\nThank you.", 'Letter generated with expected output on first checkin' );
6448     is($notice->to_address, $patron->smsalertnumber, "Letter has the correct to_address set to smsalertnumber for SMS type notices");
6449
6450     # Checkout an item, mark it returned, generate a notice
6451     my $issue_2 = AddIssue( $patron, $item->barcode);
6452     MarkIssueReturned( $patron->borrowernumber, $item->itemnumber, undef, 0, { skip_record_index => 1} );
6453     C4::Circulation::SendCirculationAlert({
6454         type => 'CHECKIN',
6455         item => $item->unblessed,
6456         borrower => $patron->unblessed,
6457         branch => $library->branchcode,
6458         issue => $issue_2
6459     });
6460     $notice->discard_changes();
6461     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' );
6462
6463 };
6464
6465 subtest "GetSoonestRenewDate tests" => sub {
6466     plan tests => 6;
6467     Koha::CirculationRules->set_rule(
6468         {
6469             categorycode => undef,
6470             branchcode   => undef,
6471             itemtype     => undef,
6472             rule_name    => 'norenewalbefore',
6473             rule_value   => '7',
6474         }
6475     );
6476     my $patron = $builder->build_object(
6477         {
6478             class => 'Koha::Patrons',
6479             value => {
6480                 autorenew_checkouts => 1,
6481             }
6482         }
6483     );
6484     my $item = $builder->build_sample_item();
6485     my $issue = AddIssue( $patron, $item->barcode);
6486     my $datedue = dt_from_string( $issue->date_due() );
6487
6488     # Bug 14395
6489     # Test 'exact time' setting for syspref NoRenewalBeforePrecision
6490     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
6491     is(
6492         GetSoonestRenewDate( $patron, $issue ),
6493         $datedue->clone->add( days => -7 ),
6494         'Bug 14395: Renewals permitted 7 days before due date, as expected'
6495     );
6496
6497     # Bug 14395
6498     # Test 'date' setting for syspref NoRenewalBeforePrecision
6499     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6500     is(
6501         GetSoonestRenewDate( $patron, $issue ),
6502         $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
6503         'Bug 14395: Renewals permitted 7 days before due date, as expected'
6504     );
6505
6506
6507     Koha::CirculationRules->set_rule(
6508         {
6509             categorycode => undef,
6510             branchcode   => undef,
6511             itemtype     => undef,
6512             rule_name    => 'norenewalbefore',
6513             rule_value   => undef,
6514         }
6515     );
6516
6517     is(
6518         GetSoonestRenewDate( $patron, $issue ),
6519         dt_from_string,
6520         'Checkouts without auto-renewal can be renewed immediately if no norenewalbefore'
6521     );
6522
6523     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6524     $issue->auto_renew(1)->store;
6525     is(
6526         GetSoonestRenewDate( $patron, $issue, 1 ),
6527         $datedue->clone->truncate( to => 'day' ),
6528         'Checkouts with auto-renewal can be renewed earliest on due date if noautorenewalbefore'
6529     );
6530     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact' );
6531     is(
6532         GetSoonestRenewDate( $patron, $issue, 1 ),
6533         $datedue,
6534         'Checkouts with auto-renewal can be renewed earliest on due date if noautorenewalbefore'
6535     );
6536
6537     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
6538     Koha::CirculationRules->set_rule(
6539         {
6540             categorycode => undef,
6541             branchcode   => undef,
6542             itemtype     => undef,
6543             rule_name    => 'norenewalbefore',
6544             rule_value   => 1,
6545         }
6546     );
6547     $issue->date_due(dt_from_string)->store;
6548     is(
6549         GetSoonestRenewDate( $patron, $issue ),
6550         dt_from_string->subtract( days => 1 )->truncate( to => 'day' ),
6551         'Checkouts with auto-renewal can be renewed 1 day before due date if no renewalbefore = 1 and precision = "date"'
6552     );
6553
6554 };
6555
6556 subtest "CanBookBeIssued + needsconfirmation message" => sub {
6557     plan tests => 4;
6558
6559     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
6560     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
6561     my $biblio = $builder->build_object({ class => 'Koha::Biblios' });
6562     my $biblioitem = $builder->build_object({ class => 'Koha::Biblioitems', value => { biblionumber => $biblio->biblionumber }});
6563     my $item = $builder->build_object({ class => 'Koha::Items' , value => { itype => $itemtype, biblionumber => $biblio->biblionumber }});
6564
6565     my $hold = $builder->build_object({ class => 'Koha::Holds', value => {
6566         biblionumber => $item->biblionumber,
6567         branchcode => $library->branchcode,
6568         itemnumber => undef,
6569         itemtype => undef,
6570         priority => 1,
6571         found => undef,
6572         suspend => 0,
6573         item_group_id => undef
6574     }});
6575
6576     my ( $error, $needsconfirmation, $alerts, $messages );
6577
6578     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6579     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold exists.");
6580
6581     $hold->priority(0)->store();
6582
6583     $hold->found("W")->store();
6584     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6585     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is waiting.");
6586
6587     $hold->found("T")->store();
6588     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6589     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is being transferred.");
6590
6591     $hold->found("P")->store();
6592     ( $error, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( $patron, $item->barcode );
6593     is($needsconfirmation->{resbranchcode}, $hold->branchcode, "Branchcodes match when hold is being processed.");
6594 };
6595
6596 subtest 'Tests for BlockReturnOfWithdrawnItems' => sub {
6597
6598     plan tests => 1;
6599
6600     t::lib::Mocks::mock_preference('BlockReturnOfWithdrawnItems', 1);
6601     t::lib::Mocks::mock_preference('RecordLocalUseOnReturn', 0);
6602     my $item = $builder->build_sample_item();
6603     $item->withdrawn(1)->itemlost(1)->store;
6604     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6605     is_deeply(
6606         \@return,
6607         [ 0, { NotIssued => $item->barcode, withdrawn => 1 }, undef, {} ], "Item returned as withdrawn, no other messages");
6608 };
6609
6610 subtest 'Tests for transfer not in transit' => sub {
6611
6612     plan tests => 2;
6613
6614
6615     # These tests are to ensure a 'pending' transfer, generated by
6616     # stock rotation, will be advanced when checked in
6617
6618     my $item = $builder->build_sample_item();
6619     my $transfer = $builder->build_object({ class => 'Koha::Item::Transfers', value => {
6620         itemnumber => $item->id,
6621         reason => 'StockrotationRepatriation',
6622         datesent => undef,
6623         frombranch => $item->homebranch,
6624     }});
6625     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6626     is_deeply(
6627         \@return,
6628         [ 0, { WasTransfered => $transfer->tobranch, TransferTrigger => 'StockrotationRepatriation', NotIssued => $item->barcode }, undef, {} ], "Item is reported to have been transferred");
6629
6630     $transfer->discard_changes;
6631     ok( $transfer->datesent, 'The datesent field is populated, i.e. transfer is initiated');
6632
6633 };
6634
6635 subtest 'Tests for RecordLocalUseOnReturn' => sub {
6636
6637     plan tests => 5;
6638
6639     t::lib::Mocks::mock_preference( 'RecordLocalUseOnReturn', 0 );
6640     my $item = $builder->build_sample_item();
6641
6642     my $item_2 = $builder->build_sample_item(
6643         {
6644             onloan => undef,
6645         }
6646     );
6647
6648     $item->withdrawn(1)->itemlost(1)->store;
6649     my @return = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6650     is_deeply(
6651         \@return,
6652         [ 0, { NotIssued => $item->barcode, withdrawn => 1 }, undef, {} ],
6653         "RecordLocalUSeOnReturn is off, no local use recorded"
6654     );
6655
6656     AddReturn( $item_2->barcode, $item_2->homebranch );
6657     $item_2->discard_changes;    # refresh
6658     is( $item_2->localuse, undef, 'Without RecordLocalUseOnReturn no localuse is recorded.' );
6659
6660     t::lib::Mocks::mock_preference( 'RecordLocalUseOnReturn', 1 );
6661     my @return2 = AddReturn( $item->barcode, $item->homebranch, 0, undef );
6662     is_deeply(
6663         \@return2,
6664         [ 0, { NotIssued => $item->barcode, withdrawn => 1, LocalUse => 1 }, undef, {} ], "Local use is recorded"
6665     );
6666
6667     AddReturn( $item_2->barcode, $item_2->homebranch );
6668     $item_2->discard_changes;    # refresh
6669     is( $item_2->localuse, 1, 'With RecordLocalUseOnReturn localuse is recorded.' );
6670
6671     AddReturn( $item_2->barcode, $item_2->homebranch );
6672     $item_2->discard_changes;    # refresh
6673     is( $item_2->localuse, 2, 'With RecordLocalUseOnReturn localuse is recorded.' );
6674 };
6675
6676 subtest 'Test CanBookBeIssued param ignore_reserves (Bug 35322)' => sub {
6677     plan tests => 4;
6678
6679     my $homebranch    = $builder->build( { source => 'Branch' } );
6680     my $holdingbranch = $builder->build( { source => 'Branch' } );
6681     my $patron_1      = $builder->build_object(
6682         { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
6683     my $patron_2 = $builder->build_object(
6684         { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
6685
6686     my $item = $builder->build_sample_item(
6687         {
6688             homebranch    => $homebranch->{branchcode},
6689             holdingbranch => $holdingbranch->{branchcode},
6690         }
6691     );
6692
6693     Koha::CirculationRules->search()->delete();
6694     Koha::CirculationRules->set_rules(
6695         {
6696             categorycode => undef,
6697             itemtype     => undef,
6698             branchcode   => undef,
6699             rules        => {
6700                 reservesallowed => 25,
6701                 issuelength     => 14,
6702                 lengthunit      => 'days',
6703                 renewalsallowed => 1,
6704                 renewalperiod   => 7,
6705                 chargeperiod    => 1,
6706                 maxissueqty     => 20,
6707             }
6708         }
6709     );
6710
6711     my $reserve_id = AddReserve(
6712         {
6713             branchcode       => $homebranch,
6714             borrowernumber   => $patron_1->id,
6715             biblionumber     => $item->biblionumber,
6716             priority         => 1,
6717             reservation_date => dt_from_string,
6718             expiration_date  => dt_from_string,
6719             itemnumber       => $item->id,
6720             found            => 'W',
6721         }
6722     );
6723
6724     set_userenv($holdingbranch);
6725
6726     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode, undef, undef, 0 );
6727     is(
6728         keys(%$error) + keys(%$alerts), 0,
6729         'There should not be any errors or alerts (impossible)' . str( $error, $question, $alerts )
6730     );
6731     is( exists $question->{RESERVE_WAITING}, 1, 'RESERVE_WAITING is set' );
6732
6733     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->barcode, undef, undef, 1 );
6734     is(
6735         keys(%$error) + keys(%$alerts), 0,
6736         'There should not be any errors or alerts (impossible)' . str( $error, $question, $alerts )
6737     );
6738     isnt( exists $question->{RESERVE_WAITING}, 1, 'RESERVE_WAITING is not set' );
6739
6740 };
6741
6742
6743 $schema->storage->txn_rollback;
6744 C4::Context->clear_syspref_cache();
6745 $branches = Koha::Libraries->search();
6746 for my $branch ( $branches->next ) {
6747     my $key = $branch->branchcode . "_holidays";
6748     $cache->clear_from_cache($key);
6749 }