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