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