Bug 25723: Update cache flushing calls
[koha-equinox.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 => 47;
22 use Test::MockModule;
23 use Test::Deep qw( cmp_deeply );
24
25 use Data::Dumper;
26 use DateTime;
27 use Time::Fake;
28 use POSIX qw( floor );
29 use t::lib::Mocks;
30 use t::lib::TestBuilder;
31
32 use C4::Accounts;
33 use C4::Calendar;
34 use C4::Circulation;
35 use C4::Biblio;
36 use C4::Items;
37 use C4::Log;
38 use C4::Reserves;
39 use C4::Overdues qw(UpdateFine CalcFine);
40 use Koha::DateUtils;
41 use Koha::Database;
42 use Koha::Items;
43 use Koha::Item::Transfers;
44 use Koha::Checkouts;
45 use Koha::Patrons;
46 use Koha::Holds;
47 use Koha::CirculationRules;
48 use Koha::Subscriptions;
49 use Koha::Account::Lines;
50 use Koha::Account::Offsets;
51 use Koha::ActionLogs;
52
53 sub set_userenv {
54     my ( $library ) = @_;
55     t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
56 }
57
58 sub str {
59     my ( $error, $question, $alert ) = @_;
60     my $s;
61     $s  = %$error    ? ' (error: '    . join( ' ', keys %$error    ) . ')' : '';
62     $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
63     $s .= %$alert    ? ' (alert: '    . join( ' ', keys %$alert    ) . ')' : '';
64     return $s;
65 }
66
67 sub test_debarment_on_checkout {
68     my ($params) = @_;
69     my $item     = $params->{item};
70     my $library  = $params->{library};
71     my $patron   = $params->{patron};
72     my $due_date = $params->{due_date} || dt_from_string;
73     my $return_date = $params->{return_date} || dt_from_string;
74     my $expected_expiration_date = $params->{expiration_date};
75
76     $expected_expiration_date = output_pref(
77         {
78             dt         => $expected_expiration_date,
79             dateformat => 'sql',
80             dateonly   => 1,
81         }
82     );
83     my @caller      = caller;
84     my $line_number = $caller[2];
85     AddIssue( $patron, $item->{barcode}, $due_date );
86
87     my ( undef, $message ) = AddReturn( $item->{barcode}, $library->{branchcode}, undef, $return_date );
88     is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
89         or diag('AddReturn returned message ' . Dumper $message );
90     my $debarments = Koha::Patron::Debarments::GetDebarments(
91         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
92     is( scalar(@$debarments), 1, 'Test at line ' . $line_number );
93
94     is( $debarments->[0]->{expiration},
95         $expected_expiration_date, 'Test at line ' . $line_number );
96     Koha::Patron::Debarments::DelUniqueDebarment(
97         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
98 };
99
100 my $schema = Koha::Database->schema;
101 $schema->storage->txn_begin;
102 my $builder = t::lib::TestBuilder->new;
103 my $dbh = C4::Context->dbh;
104
105 # Prevent random failures by mocking ->now
106 my $now_value       = dt_from_string;
107 my $mocked_datetime = Test::MockModule->new('DateTime');
108 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
109
110 my $cache = Koha::Caches->get_instance();
111 $dbh->do(q|DELETE FROM special_holidays|);
112 $dbh->do(q|DELETE FROM repeatable_holidays|);
113 my $branches = Koha::Libraries->search();
114 for my $branch ( $branches->next ) {
115     my $key = $branch->branchcode . "_holidays";
116     $cache->clear_from_cache($key);
117 }
118
119 # Start with a clean slate
120 $dbh->do('DELETE FROM issues');
121 $dbh->do('DELETE FROM borrowers');
122
123 my $library = $builder->build({
124     source => 'Branch',
125 });
126 my $library2 = $builder->build({
127     source => 'Branch',
128 });
129 my $itemtype = $builder->build(
130     {
131         source => 'Itemtype',
132         value  => {
133             notforloan          => undef,
134             rentalcharge        => 0,
135             rentalcharge_daily => 0,
136             defaultreplacecost  => undef,
137             processfee          => undef
138         }
139     }
140 )->{itemtype};
141 my $patron_category = $builder->build(
142     {
143         source => 'Category',
144         value  => {
145             category_type                 => 'P',
146             enrolmentfee                  => 0,
147             BlockExpiredPatronOpacActions => -1, # Pick the pref value
148         }
149     }
150 );
151
152 my $CircControl = C4::Context->preference('CircControl');
153 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
154
155 my $item = {
156     homebranch => $library2->{branchcode},
157     holdingbranch => $library2->{branchcode}
158 };
159
160 my $borrower = {
161     branchcode => $library2->{branchcode}
162 };
163
164 t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
165
166 # No userenv, PickupLibrary
167 t::lib::Mocks::mock_preference('IndependentBranches', '0');
168 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
169 is(
170     C4::Context->preference('CircControl'),
171     'PickupLibrary',
172     'CircControl changed to PickupLibrary'
173 );
174 is(
175     C4::Circulation::_GetCircControlBranch($item, $borrower),
176     $item->{$HomeOrHoldingBranch},
177     '_GetCircControlBranch returned item branch (no userenv defined)'
178 );
179
180 # No userenv, PatronLibrary
181 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
182 is(
183     C4::Context->preference('CircControl'),
184     'PatronLibrary',
185     'CircControl changed to PatronLibrary'
186 );
187 is(
188     C4::Circulation::_GetCircControlBranch($item, $borrower),
189     $borrower->{branchcode},
190     '_GetCircControlBranch returned borrower branch'
191 );
192
193 # No userenv, ItemHomeLibrary
194 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
195 is(
196     C4::Context->preference('CircControl'),
197     'ItemHomeLibrary',
198     'CircControl changed to ItemHomeLibrary'
199 );
200 is(
201     $item->{$HomeOrHoldingBranch},
202     C4::Circulation::_GetCircControlBranch($item, $borrower),
203     '_GetCircControlBranch returned item branch'
204 );
205
206 # Now, set a userenv
207 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
208 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
209
210 # Userenv set, PickupLibrary
211 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
212 is(
213     C4::Context->preference('CircControl'),
214     'PickupLibrary',
215     'CircControl changed to PickupLibrary'
216 );
217 is(
218     C4::Circulation::_GetCircControlBranch($item, $borrower),
219     $library2->{branchcode},
220     '_GetCircControlBranch returned current branch'
221 );
222
223 # Userenv set, PatronLibrary
224 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
225 is(
226     C4::Context->preference('CircControl'),
227     'PatronLibrary',
228     'CircControl changed to PatronLibrary'
229 );
230 is(
231     C4::Circulation::_GetCircControlBranch($item, $borrower),
232     $borrower->{branchcode},
233     '_GetCircControlBranch returned borrower branch'
234 );
235
236 # Userenv set, ItemHomeLibrary
237 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
238 is(
239     C4::Context->preference('CircControl'),
240     'ItemHomeLibrary',
241     'CircControl changed to ItemHomeLibrary'
242 );
243 is(
244     C4::Circulation::_GetCircControlBranch($item, $borrower),
245     $item->{$HomeOrHoldingBranch},
246     '_GetCircControlBranch returned item branch'
247 );
248
249 # Reset initial configuration
250 t::lib::Mocks::mock_preference('CircControl', $CircControl);
251 is(
252     C4::Context->preference('CircControl'),
253     $CircControl,
254     'CircControl reset to its initial value'
255 );
256
257 # Set a simple circ policy
258 $dbh->do('DELETE FROM circulation_rules');
259 Koha::CirculationRules->set_rules(
260     {
261         categorycode => undef,
262         branchcode   => undef,
263         itemtype     => undef,
264         rules        => {
265             reservesallowed => 25,
266             issuelength     => 14,
267             lengthunit      => 'days',
268             renewalsallowed => 1,
269             renewalperiod   => 7,
270             norenewalbefore => undef,
271             auto_renew      => 0,
272             fine            => .10,
273             chargeperiod    => 1,
274         }
275     }
276 );
277
278 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
279 subtest "CanBookBeRenewed tests" => sub {
280     plan tests => 83;
281
282     C4::Context->set_preference('ItemsDeniedRenewal','');
283     # Generate test biblio
284     my $biblio = $builder->build_sample_biblio();
285
286     my $branch = $library2->{branchcode};
287
288     my $item_1 = $builder->build_sample_item(
289         {
290             biblionumber     => $biblio->biblionumber,
291             library          => $branch,
292             replacementprice => 12.00,
293             itype            => $itemtype
294         }
295     );
296     $reused_itemnumber_1 = $item_1->itemnumber;
297
298     my $item_2 = $builder->build_sample_item(
299         {
300             biblionumber     => $biblio->biblionumber,
301             library          => $branch,
302             replacementprice => 23.00,
303             itype            => $itemtype
304         }
305     );
306     $reused_itemnumber_2 = $item_2->itemnumber;
307
308     my $item_3 = $builder->build_sample_item(
309         {
310             biblionumber     => $biblio->biblionumber,
311             library          => $branch,
312             replacementprice => 23.00,
313             itype            => $itemtype
314         }
315     );
316
317     # Create borrowers
318     my %renewing_borrower_data = (
319         firstname =>  'John',
320         surname => 'Renewal',
321         categorycode => $patron_category->{categorycode},
322         branchcode => $branch,
323     );
324
325     my %reserving_borrower_data = (
326         firstname =>  'Katrin',
327         surname => 'Reservation',
328         categorycode => $patron_category->{categorycode},
329         branchcode => $branch,
330     );
331
332     my %hold_waiting_borrower_data = (
333         firstname =>  'Kyle',
334         surname => 'Reservation',
335         categorycode => $patron_category->{categorycode},
336         branchcode => $branch,
337     );
338
339     my %restricted_borrower_data = (
340         firstname =>  'Alice',
341         surname => 'Reservation',
342         categorycode => $patron_category->{categorycode},
343         debarred => '3228-01-01',
344         branchcode => $branch,
345     );
346
347     my %expired_borrower_data = (
348         firstname =>  'Ça',
349         surname => 'Glisse',
350         categorycode => $patron_category->{categorycode},
351         branchcode => $branch,
352         dateexpiry => dt_from_string->subtract( months => 1 ),
353     );
354
355     my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
356     my $reserving_borrowernumber = Koha::Patron->new(\%reserving_borrower_data)->store->borrowernumber;
357     my $hold_waiting_borrowernumber = Koha::Patron->new(\%hold_waiting_borrower_data)->store->borrowernumber;
358     my $restricted_borrowernumber = Koha::Patron->new(\%restricted_borrower_data)->store->borrowernumber;
359     my $expired_borrowernumber = Koha::Patron->new(\%expired_borrower_data)->store->borrowernumber;
360
361     my $renewing_borrower_obj = Koha::Patrons->find( $renewing_borrowernumber );
362     my $renewing_borrower = $renewing_borrower_obj->unblessed;
363     my $restricted_borrower = Koha::Patrons->find( $restricted_borrowernumber )->unblessed;
364     my $expired_borrower = Koha::Patrons->find( $expired_borrowernumber )->unblessed;
365
366     my $bibitems       = '';
367     my $priority       = '1';
368     my $resdate        = undef;
369     my $expdate        = undef;
370     my $notes          = '';
371     my $checkitem      = undef;
372     my $found          = undef;
373
374     my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
375     my $datedue = dt_from_string( $issue->date_due() );
376     is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
377
378     my $issue2 = AddIssue( $renewing_borrower, $item_2->barcode);
379     $datedue = dt_from_string( $issue->date_due() );
380     is (defined $issue2, 1, "Item 2 checked out, due date: " . $issue2->date_due());
381
382
383     my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
384     is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}");
385
386     my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
387     is( $renewokay, 1, 'Can renew, no holds for this title or item');
388
389
390     # Biblio-level hold, renewal test
391     AddReserve(
392         {
393             branchcode       => $branch,
394             borrowernumber   => $reserving_borrowernumber,
395             biblionumber     => $biblio->biblionumber,
396             priority         => $priority,
397             reservation_date => $resdate,
398             expiration_date  => $expdate,
399             notes            => $notes,
400             itemnumber       => $checkitem,
401             found            => $found,
402         }
403     );
404
405     # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
406     Koha::CirculationRules->set_rule(
407         {
408             categorycode => undef,
409             branchcode   => undef,
410             itemtype     => undef,
411             rule_name    => 'onshelfholds',
412             rule_value   => '1',
413         }
414     );
415     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
416     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
417     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
418     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
419     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
420
421     # Now let's add an item level hold, we should no longer be able to renew the item
422     my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
423         {
424             borrowernumber => $hold_waiting_borrowernumber,
425             biblionumber   => $biblio->biblionumber,
426             itemnumber     => $item_1->itemnumber,
427             branchcode     => $branch,
428             priority       => 3,
429         }
430     );
431     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
432     is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
433     $hold->delete();
434
435     # 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
436     # be able to renew these items
437     $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
438         {
439             borrowernumber => $hold_waiting_borrowernumber,
440             biblionumber   => $biblio->biblionumber,
441             itemnumber     => $item_3->itemnumber,
442             branchcode     => $branch,
443             priority       => 0,
444             found          => 'W'
445         }
446     );
447     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
448     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
449     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
450     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
451     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
452
453     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
454     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
455     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
456
457     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
458     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
459     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
460
461     my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
462     my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber )->unblessed;
463     AddIssue($reserving_borrower, $item_3->barcode);
464     my $reserve = $dbh->selectrow_hashref(
465         'SELECT * FROM old_reserves WHERE reserve_id = ?',
466         { Slice => {} },
467         $reserveid
468     );
469     is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
470
471     # Item-level hold, renewal test
472     AddReserve(
473         {
474             branchcode       => $branch,
475             borrowernumber   => $reserving_borrowernumber,
476             biblionumber     => $biblio->biblionumber,
477             priority         => $priority,
478             reservation_date => $resdate,
479             expiration_date  => $expdate,
480             notes            => $notes,
481             itemnumber       => $item_1->itemnumber,
482             found            => $found,
483         }
484     );
485
486     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
487     is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
488     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
489
490     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber, 1);
491     is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
492
493     # Items can't fill hold for reasons
494     $item_1->notforloan(1)->store;
495     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
496     is( $renewokay, 1, 'Can renew, item is marked not for loan, hold does not block');
497     $item_1->set({notforloan => 0, itype => $itemtype })->store;
498
499     # FIXME: Add more for itemtype not for loan etc.
500
501     # Restricted users cannot renew when RestrictionBlockRenewing is enabled
502     my $item_5 = $builder->build_sample_item(
503         {
504             biblionumber     => $biblio->biblionumber,
505             library          => $branch,
506             replacementprice => 23.00,
507             itype            => $itemtype,
508         }
509     );
510     my $datedue5 = AddIssue($restricted_borrower, $item_5->barcode);
511     is (defined $datedue5, 1, "Item with date due checked out, due date: $datedue5");
512
513     t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
514     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
515     is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
516     ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $item_5->itemnumber);
517     is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
518
519     # Users cannot renew an overdue item
520     my $item_6 = $builder->build_sample_item(
521         {
522             biblionumber     => $biblio->biblionumber,
523             library          => $branch,
524             replacementprice => 23.00,
525             itype            => $itemtype,
526         }
527     );
528
529     my $item_7 = $builder->build_sample_item(
530         {
531             biblionumber     => $biblio->biblionumber,
532             library          => $branch,
533             replacementprice => 23.00,
534             itype            => $itemtype,
535         }
536     );
537
538     my $datedue6 = AddIssue( $renewing_borrower, $item_6->barcode);
539     is (defined $datedue6, 1, "Item 2 checked out, due date: ".$datedue6->date_due);
540
541     my $now = dt_from_string();
542     my $five_weeks = DateTime::Duration->new(weeks => 5);
543     my $five_weeks_ago = $now - $five_weeks;
544     t::lib::Mocks::mock_preference('finesMode', 'production');
545
546     my $passeddatedue1 = AddIssue($renewing_borrower, $item_7->barcode, $five_weeks_ago);
547     is (defined $passeddatedue1, 1, "Item with passed date due checked out, due date: " . $passeddatedue1->date_due);
548
549     my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower->{categorycode}, $branch, $five_weeks_ago, $now );
550     C4::Overdues::UpdateFine(
551         {
552             issue_id       => $passeddatedue1->id(),
553             itemnumber     => $item_7->itemnumber,
554             borrowernumber => $renewing_borrower->{borrowernumber},
555             amount         => $fine,
556             due            => Koha::DateUtils::output_pref($five_weeks_ago)
557         }
558     );
559
560     t::lib::Mocks::mock_preference('RenewalLog', 0);
561     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
562     my %params_renewal = (
563         timestamp => { -like => $date . "%" },
564         module => "CIRCULATION",
565         action => "RENEWAL",
566     );
567     my %params_issue = (
568         timestamp => { -like => $date . "%" },
569         module => "CIRCULATION",
570         action => "ISSUE"
571     );
572     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );
573     my $dt = dt_from_string();
574     Time::Fake->offset( $dt->epoch );
575     my $datedue1 = AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
576     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
577     is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
578     isnt (DateTime->compare($datedue1, $dt), 0, "AddRenewal returned a good duedate");
579     Time::Fake->reset;
580
581     t::lib::Mocks::mock_preference('RenewalLog', 1);
582     $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
583     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
584     AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
585     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
586     is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
587
588     my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
589     is( $fines->count, 2, 'AddRenewal left both fines' );
590     isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
591     isnt( $fines->next->status, 'UNRETURNED', 'Fine on renewed item is closed out properly' );
592     $fines->delete();
593
594
595     my $old_issue_log_size = Koha::ActionLogs->count( \%params_issue );
596     my $old_renew_log_size = Koha::ActionLogs->count( \%params_renewal );
597     AddIssue( $renewing_borrower,$item_7->barcode,Koha::DateUtils::output_pref({str=>$datedue6->date_due, dateformat =>'iso'}),0,$date, 0, undef );
598     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
599     is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
600     $new_log_size = Koha::ActionLogs->count( \%params_issue );
601     is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
602
603     $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
604     $fines->delete();
605
606     t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
607     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
608     is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
609     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
610     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
611
612
613     $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
614     $hold->cancel;
615
616     # Bug 14101
617     # Test automatic renewal before value for "norenewalbefore" in policy is set
618     # In this case automatic renewal is not permitted prior to due date
619     my $item_4 = $builder->build_sample_item(
620         {
621             biblionumber     => $biblio->biblionumber,
622             library          => $branch,
623             replacementprice => 16.00,
624             itype            => $itemtype,
625         }
626     );
627
628     $issue = AddIssue( $renewing_borrower, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
629     ( $renewokay, $error ) =
630       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
631     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
632     is( $error, 'auto_too_soon',
633         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
634     AddReserve(
635         {
636             branchcode       => $branch,
637             borrowernumber   => $reserving_borrowernumber,
638             biblionumber     => $biblio->biblionumber,
639             itemnumber       => $bibitems,
640             priority         => $priority,
641             reservation_date => $resdate,
642             expiration_date  => $expdate,
643             notes            => $notes,
644             title            => 'a title',
645             itemnumber       => $item_4->itemnumber,
646             found            => $found
647         }
648     );
649     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
650     is( $renewokay, 0, 'Still should not be able to renew' );
651     is( $error, 'auto_too_soon', 'returned code is auto_too_soon, reserve not checked' );
652     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
653     is( $renewokay, 0, 'Still should not be able to renew' );
654     is( $error, 'on_reserve', 'returned code is on_reserve, auto_too_soon limit is overridden' );
655     $dbh->do('UPDATE circulation_rules SET rule_value = 0 where rule_name = "norenewalbefore"');
656     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber, 1 );
657     is( $renewokay, 0, 'Still should not be able to renew' );
658     is( $error, 'on_reserve', 'returned code is on_reserve, auto_renew only happens if not on reserve' );
659     ModReserveCancelAll($item_4->itemnumber, $reserving_borrowernumber);
660
661
662
663     $renewing_borrower_obj->autorenew_checkouts(0)->store;
664     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
665     is( $renewokay, 1, 'No renewal before is undef, but patron opted out of auto_renewal' );
666     $renewing_borrower_obj->autorenew_checkouts(1)->store;
667
668
669     # Bug 7413
670     # Test premature manual renewal
671     Koha::CirculationRules->set_rule(
672         {
673             categorycode => undef,
674             branchcode   => undef,
675             itemtype     => undef,
676             rule_name    => 'norenewalbefore',
677             rule_value   => '7',
678         }
679     );
680
681     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
682     is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
683     is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
684
685     # Bug 14395
686     # Test 'exact time' setting for syspref NoRenewalBeforePrecision
687     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
688     is(
689         GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
690         $datedue->clone->add( days => -7 ),
691         'Bug 14395: Renewals permitted 7 days before due date, as expected'
692     );
693
694     # Bug 14395
695     # Test 'date' setting for syspref NoRenewalBeforePrecision
696     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
697     is(
698         GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
699         $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
700         'Bug 14395: Renewals permitted 7 days before due date, as expected'
701     );
702
703     # Bug 14101
704     # Test premature automatic renewal
705     ( $renewokay, $error ) =
706       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
707     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
708     is( $error, 'auto_too_soon',
709         'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
710     );
711
712     $renewing_borrower_obj->autorenew_checkouts(0)->store;
713     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
714     is( $renewokay, 0, 'No renewal before is 7, patron opted out of auto_renewal still cannot renew early' );
715     is( $error, 'too_soon', 'Error is too_soon, no auto' );
716     $renewing_borrower_obj->autorenew_checkouts(1)->store;
717
718     # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
719     # and test automatic renewal again
720     $dbh->do(q{UPDATE circulation_rules SET rule_value = '0' WHERE rule_name = 'norenewalbefore'});
721     ( $renewokay, $error ) =
722       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
723     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
724     is( $error, 'auto_too_soon',
725         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
726     );
727
728     $renewing_borrower_obj->autorenew_checkouts(0)->store;
729     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
730     is( $renewokay, 0, 'No renewal before is 0, patron opted out of auto_renewal still cannot renew early' );
731     is( $error, 'too_soon', 'Error is too_soon, no auto' );
732     $renewing_borrower_obj->autorenew_checkouts(1)->store;
733
734     # Change policy so that loans can be renewed 99 days prior to the due date
735     # and test automatic renewal again
736     $dbh->do(q{UPDATE circulation_rules SET rule_value = '99' WHERE rule_name = 'norenewalbefore'});
737     ( $renewokay, $error ) =
738       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
739     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
740     is( $error, 'auto_renew',
741         'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
742     );
743
744     $renewing_borrower_obj->autorenew_checkouts(0)->store;
745     ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
746     is( $renewokay, 1, 'No renewal before is 99, patron opted out of auto_renewal so can renew' );
747     $renewing_borrower_obj->autorenew_checkouts(1)->store;
748
749     subtest "too_late_renewal / no_auto_renewal_after" => sub {
750         plan tests => 14;
751         my $item_to_auto_renew = $builder->build(
752             {   source => 'Item',
753                 value  => {
754                     biblionumber  => $biblio->biblionumber,
755                     homebranch    => $branch,
756                     holdingbranch => $branch,
757                 }
758             }
759         );
760
761         my $ten_days_before = dt_from_string->add( days => -10 );
762         my $ten_days_ahead  = dt_from_string->add( days => 10 );
763         AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
764
765         Koha::CirculationRules->set_rules(
766             {
767                 categorycode => undef,
768                 branchcode   => undef,
769                 itemtype     => undef,
770                 rules        => {
771                     norenewalbefore       => '7',
772                     no_auto_renewal_after => '9',
773                 }
774             }
775         );
776         ( $renewokay, $error ) =
777           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
778         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
779         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
780
781         Koha::CirculationRules->set_rules(
782             {
783                 categorycode => undef,
784                 branchcode   => undef,
785                 itemtype     => undef,
786                 rules        => {
787                     norenewalbefore       => '7',
788                     no_auto_renewal_after => '10',
789                 }
790             }
791         );
792         ( $renewokay, $error ) =
793           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
794         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
795         is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
796
797         Koha::CirculationRules->set_rules(
798             {
799                 categorycode => undef,
800                 branchcode   => undef,
801                 itemtype     => undef,
802                 rules        => {
803                     norenewalbefore       => '7',
804                     no_auto_renewal_after => '11',
805                 }
806             }
807         );
808         ( $renewokay, $error ) =
809           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
810         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
811         is( $error, 'auto_too_soon', 'Cannot auto renew, too soon - no_auto_renewal_after is defined(returned code is auto_too_soon)' );
812
813         Koha::CirculationRules->set_rules(
814             {
815                 categorycode => undef,
816                 branchcode   => undef,
817                 itemtype     => undef,
818                 rules        => {
819                     norenewalbefore       => '10',
820                     no_auto_renewal_after => '11',
821                 }
822             }
823         );
824         ( $renewokay, $error ) =
825           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
826         is( $renewokay, 0,            'Do not renew, renewal is automatic' );
827         is( $error,     'auto_renew', 'Cannot renew, renew is automatic' );
828
829         Koha::CirculationRules->set_rules(
830             {
831                 categorycode => undef,
832                 branchcode   => undef,
833                 itemtype     => undef,
834                 rules        => {
835                     norenewalbefore       => '10',
836                     no_auto_renewal_after => undef,
837                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
838                 }
839             }
840         );
841         ( $renewokay, $error ) =
842           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
843         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
844         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
845
846         Koha::CirculationRules->set_rules(
847             {
848                 categorycode => undef,
849                 branchcode   => undef,
850                 itemtype     => undef,
851                 rules        => {
852                     norenewalbefore       => '7',
853                     no_auto_renewal_after => '15',
854                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => -1 ),
855                 }
856             }
857         );
858         ( $renewokay, $error ) =
859           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
860         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
861         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
862
863         Koha::CirculationRules->set_rules(
864             {
865                 categorycode => undef,
866                 branchcode   => undef,
867                 itemtype     => undef,
868                 rules        => {
869                     norenewalbefore       => '10',
870                     no_auto_renewal_after => undef,
871                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 1 ),
872                 }
873             }
874         );
875         ( $renewokay, $error ) =
876           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
877         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
878         is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
879     };
880
881     subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew & OPACFineNoRenewalsIncludeCredit" => sub {
882         plan tests => 10;
883         my $item_to_auto_renew = $builder->build({
884             source => 'Item',
885             value => {
886                 biblionumber => $biblio->biblionumber,
887                 homebranch       => $branch,
888                 holdingbranch    => $branch,
889             }
890         });
891
892         my $ten_days_before = dt_from_string->add( days => -10 );
893         my $ten_days_ahead = dt_from_string->add( days => 10 );
894         AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
895
896         Koha::CirculationRules->set_rules(
897             {
898                 categorycode => undef,
899                 branchcode   => undef,
900                 itemtype     => undef,
901                 rules        => {
902                     norenewalbefore       => '10',
903                     no_auto_renewal_after => '11',
904                 }
905             }
906         );
907         C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
908         C4::Context->set_preference('OPACFineNoRenewals','10');
909         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
910         my $fines_amount = 5;
911         my $account = Koha::Account->new({patron_id => $renewing_borrowernumber});
912         $account->add_debit(
913             {
914                 amount      => $fines_amount,
915                 interface   => 'test',
916                 type        => 'OVERDUE',
917                 item_id     => $item_to_auto_renew->{itemnumber},
918                 description => "Some fines"
919             }
920         )->status('RETURNED')->store;
921         ( $renewokay, $error ) =
922           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
923         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
924         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
925
926         $account->add_debit(
927             {
928                 amount      => $fines_amount,
929                 interface   => 'test',
930                 type        => 'OVERDUE',
931                 item_id     => $item_to_auto_renew->{itemnumber},
932                 description => "Some fines"
933             }
934         )->status('RETURNED')->store;
935         ( $renewokay, $error ) =
936           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
937         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
938         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
939
940         $account->add_debit(
941             {
942                 amount      => $fines_amount,
943                 interface   => 'test',
944                 type        => 'OVERDUE',
945                 item_id     => $item_to_auto_renew->{itemnumber},
946                 description => "Some fines"
947             }
948         )->status('RETURNED')->store;
949         ( $renewokay, $error ) =
950           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
951         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
952         is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
953
954         $account->add_credit(
955             {
956                 amount      => $fines_amount,
957                 interface   => 'test',
958                 type        => 'PAYMENT',
959                 description => "Some payment"
960             }
961         )->store;
962         ( $renewokay, $error ) =
963           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
964         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
965         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit'  );
966
967         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','0');
968         ( $renewokay, $error ) =
969           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
970         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
971         is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, OPACFineNoRenewalsIncludeCredit=1, patron has 15 debt, 5 credit'  );
972
973         $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
974         C4::Context->set_preference('OPACFineNoRenewalsIncludeCredit','1');
975     };
976
977     subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
978         plan tests => 6;
979         my $item_to_auto_renew = $builder->build({
980             source => 'Item',
981             value => {
982                 biblionumber => $biblio->biblionumber,
983                 homebranch       => $branch,
984                 holdingbranch    => $branch,
985             }
986         });
987
988         Koha::CirculationRules->set_rules(
989             {
990                 categorycode => undef,
991                 branchcode   => undef,
992                 itemtype     => undef,
993                 rules        => {
994                     norenewalbefore       => 10,
995                     no_auto_renewal_after => 11,
996                 }
997             }
998         );
999
1000         my $ten_days_before = dt_from_string->add( days => -10 );
1001         my $ten_days_ahead = dt_from_string->add( days => 10 );
1002
1003         # Patron is expired and BlockExpiredPatronOpacActions=0
1004         # => auto renew is allowed
1005         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
1006         my $patron = $expired_borrower;
1007         my $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1008         ( $renewokay, $error ) =
1009           CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
1010         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1011         is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
1012         Koha::Checkouts->find( $checkout->issue_id )->delete;
1013
1014
1015         # Patron is expired and BlockExpiredPatronOpacActions=1
1016         # => auto renew is not allowed
1017         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1018         $patron = $expired_borrower;
1019         $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1020         ( $renewokay, $error ) =
1021           CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
1022         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1023         is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
1024         Koha::Checkouts->find( $checkout->issue_id )->delete;
1025
1026
1027         # Patron is not expired and BlockExpiredPatronOpacActions=1
1028         # => auto renew is allowed
1029         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
1030         $patron = $renewing_borrower;
1031         $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
1032         ( $renewokay, $error ) =
1033           CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
1034         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
1035         is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
1036         Koha::Checkouts->find( $checkout->issue_id )->delete;
1037     };
1038
1039     subtest "GetLatestAutoRenewDate" => sub {
1040         plan tests => 5;
1041         my $item_to_auto_renew = $builder->build(
1042             {   source => 'Item',
1043                 value  => {
1044                     biblionumber  => $biblio->biblionumber,
1045                     homebranch    => $branch,
1046                     holdingbranch => $branch,
1047                 }
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         Koha::CirculationRules->set_rules(
1055             {
1056                 categorycode => undef,
1057                 branchcode   => undef,
1058                 itemtype     => undef,
1059                 rules        => {
1060                     norenewalbefore       => '7',
1061                     no_auto_renewal_after => '',
1062                     no_auto_renewal_after_hard_limit => undef,
1063                 }
1064             }
1065         );
1066         my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1067         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' );
1068         my $five_days_before = dt_from_string->add( days => -5 );
1069         Koha::CirculationRules->set_rules(
1070             {
1071                 categorycode => undef,
1072                 branchcode   => undef,
1073                 itemtype     => undef,
1074                 rules        => {
1075                     norenewalbefore       => '10',
1076                     no_auto_renewal_after => '5',
1077                     no_auto_renewal_after_hard_limit => undef,
1078                 }
1079             }
1080         );
1081         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1082         is( $latest_auto_renew_date->truncate( to => 'minute' ),
1083             $five_days_before->truncate( to => 'minute' ),
1084             'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
1085         );
1086         my $five_days_ahead = dt_from_string->add( days => 5 );
1087         $dbh->do(q{UPDATE circulation_rules SET rule_value = '10' WHERE rule_name = 'norenewalbefore'});
1088         $dbh->do(q{UPDATE circulation_rules SET rule_value = '15' WHERE rule_name = 'no_auto_renewal_after'});
1089         $dbh->do(q{UPDATE circulation_rules SET rule_value = NULL WHERE rule_name = 'no_auto_renewal_after_hard_limit'});
1090         Koha::CirculationRules->set_rules(
1091             {
1092                 categorycode => undef,
1093                 branchcode   => undef,
1094                 itemtype     => undef,
1095                 rules        => {
1096                     norenewalbefore       => '10',
1097                     no_auto_renewal_after => '15',
1098                     no_auto_renewal_after_hard_limit => undef,
1099                 }
1100             }
1101         );
1102         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1103         is( $latest_auto_renew_date->truncate( to => 'minute' ),
1104             $five_days_ahead->truncate( to => 'minute' ),
1105             'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
1106         );
1107         my $two_days_ahead = dt_from_string->add( days => 2 );
1108         Koha::CirculationRules->set_rules(
1109             {
1110                 categorycode => undef,
1111                 branchcode   => undef,
1112                 itemtype     => undef,
1113                 rules        => {
1114                     norenewalbefore       => '10',
1115                     no_auto_renewal_after => '',
1116                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1117                 }
1118             }
1119         );
1120         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1121         is( $latest_auto_renew_date->truncate( to => 'day' ),
1122             $two_days_ahead->truncate( to => 'day' ),
1123             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
1124         );
1125         Koha::CirculationRules->set_rules(
1126             {
1127                 categorycode => undef,
1128                 branchcode   => undef,
1129                 itemtype     => undef,
1130                 rules        => {
1131                     norenewalbefore       => '10',
1132                     no_auto_renewal_after => '15',
1133                     no_auto_renewal_after_hard_limit => dt_from_string->add( days => 2 ),
1134                 }
1135             }
1136         );
1137         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
1138         is( $latest_auto_renew_date->truncate( to => 'day' ),
1139             $two_days_ahead->truncate( to => 'day' ),
1140             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
1141         );
1142
1143     };
1144     # Too many renewals
1145
1146     # set policy to forbid renewals
1147     Koha::CirculationRules->set_rules(
1148         {
1149             categorycode => undef,
1150             branchcode   => undef,
1151             itemtype     => undef,
1152             rules        => {
1153                 norenewalbefore => undef,
1154                 renewalsallowed => 0,
1155             }
1156         }
1157     );
1158
1159     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
1160     is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
1161     is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
1162
1163     # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
1164     t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
1165     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1166
1167     C4::Overdues::UpdateFine(
1168         {
1169             issue_id       => $issue->id(),
1170             itemnumber     => $item_1->itemnumber,
1171             borrowernumber => $renewing_borrower->{borrowernumber},
1172             amount         => 15.00,
1173             type           => q{},
1174             due            => Koha::DateUtils::output_pref($datedue)
1175         }
1176     );
1177
1178     my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower->{borrowernumber} })->next();
1179     is( $line->debit_type_code, 'OVERDUE', 'Account line type is OVERDUE' );
1180     is( $line->status, 'UNRETURNED', 'Account line status is UNRETURNED' );
1181     is( $line->amountoutstanding+0, 15, 'Account line amount outstanding is 15.00' );
1182     is( $line->amount+0, 15, 'Account line amount is 15.00' );
1183     is( $line->issue_id, $issue->id, 'Account line issue id matches' );
1184
1185     my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
1186     is( $offset->type, 'OVERDUE', 'Account offset type is Fine' );
1187     is( $offset->amount+0, 15, 'Account offset amount is 15.00' );
1188
1189     t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
1190     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
1191
1192     LostItem( $item_1->itemnumber, 'test', 1 );
1193
1194     $line = Koha::Account::Lines->find($line->id);
1195     is( $line->debit_type_code, 'OVERDUE', 'Account type remains as OVERDUE' );
1196     isnt( $line->status, 'UNRETURNED', 'Account status correctly changed from UNRETURNED to RETURNED' );
1197
1198     my $item = Koha::Items->find($item_1->itemnumber);
1199     ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
1200     my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
1201     is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
1202
1203     my $total_due = $dbh->selectrow_array(
1204         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1205         undef, $renewing_borrower->{borrowernumber}
1206     );
1207
1208     is( $total_due+0, 15, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
1209
1210     C4::Context->dbh->do("DELETE FROM accountlines");
1211
1212     C4::Overdues::UpdateFine(
1213         {
1214             issue_id       => $issue2->id(),
1215             itemnumber     => $item_2->itemnumber,
1216             borrowernumber => $renewing_borrower->{borrowernumber},
1217             amount         => 15.00,
1218             type           => q{},
1219             due            => Koha::DateUtils::output_pref($datedue)
1220         }
1221     );
1222
1223     LostItem( $item_2->itemnumber, 'test', 0 );
1224
1225     my $item2 = Koha::Items->find($item_2->itemnumber);
1226     ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
1227     ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
1228
1229     $total_due = $dbh->selectrow_array(
1230         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
1231         undef, $renewing_borrower->{borrowernumber}
1232     );
1233
1234     ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
1235
1236     my $future = dt_from_string();
1237     $future->add( days => 7 );
1238     my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
1239     ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
1240
1241     # Users cannot renew any item if there is an overdue item
1242     t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
1243     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
1244     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1245     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
1246     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
1247
1248     my $manager = $builder->build_object({ class => "Koha::Patrons" });
1249     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
1250     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
1251     $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
1252     LostItem( $item_3->itemnumber, 'test', 0 );
1253     my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
1254     is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
1255     is(
1256         $accountline->description,
1257         sprintf( "%s %s %s",
1258             $item_3->biblio->title  || '',
1259             $item_3->barcode        || '',
1260             $item_3->itemcallnumber || '' ),
1261         "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1262     );
1263 };
1264
1265 subtest "GetUpcomingDueIssues" => sub {
1266     plan tests => 12;
1267
1268     my $branch   = $library2->{branchcode};
1269
1270     #Create another record
1271     my $biblio2 = $builder->build_sample_biblio();
1272
1273     #Create third item
1274     my $item_1 = Koha::Items->find($reused_itemnumber_1);
1275     my $item_2 = Koha::Items->find($reused_itemnumber_2);
1276     my $item_3 = $builder->build_sample_item(
1277         {
1278             biblionumber     => $biblio2->biblionumber,
1279             library          => $branch,
1280             itype            => $itemtype,
1281         }
1282     );
1283
1284
1285     # Create a borrower
1286     my %a_borrower_data = (
1287         firstname =>  'Fridolyn',
1288         surname => 'SOMERS',
1289         categorycode => $patron_category->{categorycode},
1290         branchcode => $branch,
1291     );
1292
1293     my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1294     my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber )->unblessed;
1295
1296     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
1297     my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
1298     my $today = DateTime->today(time_zone => C4::Context->tz());
1299
1300     my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
1301     my $datedue = dt_from_string( $issue->date_due() );
1302     my $issue2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
1303     my $datedue2 = dt_from_string( $issue->date_due() );
1304
1305     my $upcoming_dues;
1306
1307     # GetUpcomingDueIssues tests
1308     for my $i(0..1) {
1309         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1310         is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
1311     }
1312
1313     #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
1314     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
1315     is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
1316
1317     for my $i(3..5) {
1318         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
1319         is ( scalar( @$upcoming_dues ), 1,
1320             "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1321     }
1322
1323     # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1324
1325     my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1326
1327     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1328     is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1329
1330     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1331     is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1332
1333     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1334     is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1335
1336     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 }  );
1337     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1338
1339     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1340     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1341
1342     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1343     is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1344
1345 };
1346
1347 subtest "Bug 13841 - Do not create new 0 amount fines" => sub {
1348     my $branch   = $library2->{branchcode};
1349
1350     my $biblio = $builder->build_sample_biblio();
1351
1352     #Create third item
1353     my $item = $builder->build_sample_item(
1354         {
1355             biblionumber     => $biblio->biblionumber,
1356             library          => $branch,
1357             itype            => $itemtype,
1358         }
1359     );
1360
1361     # Create a borrower
1362     my %a_borrower_data = (
1363         firstname =>  'Kyle',
1364         surname => 'Hall',
1365         categorycode => $patron_category->{categorycode},
1366         branchcode => $branch,
1367     );
1368
1369     my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1370
1371     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1372     my $issue = AddIssue( $borrower, $item->barcode );
1373     UpdateFine(
1374         {
1375             issue_id       => $issue->id(),
1376             itemnumber     => $item->itemnumber,
1377             borrowernumber => $borrowernumber,
1378             amount         => 0,
1379             type           => q{}
1380         }
1381     );
1382
1383     my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1384     my $count = $hr->{count};
1385
1386     is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1387 };
1388
1389 subtest "AllowRenewalIfOtherItemsAvailable tests" => sub {
1390     $dbh->do('DELETE FROM issues');
1391     $dbh->do('DELETE FROM items');
1392     $dbh->do('DELETE FROM circulation_rules');
1393     Koha::CirculationRules->set_rules(
1394         {
1395             categorycode => undef,
1396             itemtype     => undef,
1397             branchcode   => undef,
1398             rules        => {
1399                 reservesallowed => 25,
1400                 issuelength     => 14,
1401                 lengthunit      => 'days',
1402                 renewalsallowed => 1,
1403                 renewalperiod   => 7,
1404                 norenewalbefore => undef,
1405                 auto_renew      => 0,
1406                 fine            => .10,
1407                 chargeperiod    => 1,
1408                 maxissueqty     => 20
1409             }
1410         }
1411     );
1412     my $biblio = $builder->build_sample_biblio();
1413
1414     my $item_1 = $builder->build_sample_item(
1415         {
1416             biblionumber     => $biblio->biblionumber,
1417             library          => $library2->{branchcode},
1418             itype            => $itemtype,
1419         }
1420     );
1421
1422     my $item_2= $builder->build_sample_item(
1423         {
1424             biblionumber     => $biblio->biblionumber,
1425             library          => $library2->{branchcode},
1426             itype            => $itemtype,
1427         }
1428     );
1429
1430     my $borrowernumber1 = Koha::Patron->new({
1431         firstname    => 'Kyle',
1432         surname      => 'Hall',
1433         categorycode => $patron_category->{categorycode},
1434         branchcode   => $library2->{branchcode},
1435     })->store->borrowernumber;
1436     my $borrowernumber2 = Koha::Patron->new({
1437         firstname    => 'Chelsea',
1438         surname      => 'Hall',
1439         categorycode => $patron_category->{categorycode},
1440         branchcode   => $library2->{branchcode},
1441     })->store->borrowernumber;
1442
1443     my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
1444     my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
1445
1446     my $issue = AddIssue( $borrower1, $item_1->barcode );
1447
1448     my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1449     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1450
1451     AddReserve(
1452         {
1453             branchcode     => $library2->{branchcode},
1454             borrowernumber => $borrowernumber2,
1455             biblionumber   => $biblio->biblionumber,
1456             priority       => 1,
1457         }
1458     );
1459
1460     Koha::CirculationRules->set_rules(
1461         {
1462             categorycode => undef,
1463             itemtype     => undef,
1464             branchcode   => undef,
1465             rules        => {
1466                 onshelfholds => 0,
1467             }
1468         }
1469     );
1470     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1471     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1472     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1473
1474     Koha::CirculationRules->set_rules(
1475         {
1476             categorycode => undef,
1477             itemtype     => undef,
1478             branchcode   => undef,
1479             rules        => {
1480                 onshelfholds => 0,
1481             }
1482         }
1483     );
1484     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1485     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1486     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1487
1488     Koha::CirculationRules->set_rules(
1489         {
1490             categorycode => undef,
1491             itemtype     => undef,
1492             branchcode   => undef,
1493             rules        => {
1494                 onshelfholds => 1,
1495             }
1496         }
1497     );
1498     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1499     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1500     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1501
1502     Koha::CirculationRules->set_rules(
1503         {
1504             categorycode => undef,
1505             itemtype     => undef,
1506             branchcode   => undef,
1507             rules        => {
1508                 onshelfholds => 1,
1509             }
1510         }
1511     );
1512     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1513     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1514     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1515
1516     # Setting item not checked out to be not for loan but holdable
1517     $item_2->notforloan(-1)->store;
1518
1519     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1520     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' );
1521 };
1522
1523 {
1524     # Don't allow renewing onsite checkout
1525     my $branch   = $library->{branchcode};
1526
1527     #Create another record
1528     my $biblio = $builder->build_sample_biblio();
1529
1530     my $item = $builder->build_sample_item(
1531         {
1532             biblionumber     => $biblio->biblionumber,
1533             library          => $branch,
1534             itype            => $itemtype,
1535         }
1536     );
1537
1538     my $borrowernumber = Koha::Patron->new({
1539         firstname =>  'fn',
1540         surname => 'dn',
1541         categorycode => $patron_category->{categorycode},
1542         branchcode => $branch,
1543     })->store->borrowernumber;
1544
1545     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1546
1547     my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
1548     my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $item->itemnumber );
1549     is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
1550     is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
1551 }
1552
1553 {
1554     my $library = $builder->build({ source => 'Branch' });
1555
1556     my $biblio = $builder->build_sample_biblio();
1557
1558     my $item = $builder->build_sample_item(
1559         {
1560             biblionumber     => $biblio->biblionumber,
1561             library          => $library->{branchcode},
1562             itype            => $itemtype,
1563         }
1564     );
1565
1566     my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
1567
1568     my $issue = AddIssue( $patron, $item->barcode );
1569     UpdateFine(
1570         {
1571             issue_id       => $issue->id(),
1572             itemnumber     => $item->itemnumber,
1573             borrowernumber => $patron->{borrowernumber},
1574             amount         => 1,
1575             type           => q{}
1576         }
1577     );
1578     UpdateFine(
1579         {
1580             issue_id       => $issue->id(),
1581             itemnumber     => $item->itemnumber,
1582             borrowernumber => $patron->{borrowernumber},
1583             amount         => 2,
1584             type           => q{}
1585         }
1586     );
1587     is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1588 }
1589
1590 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1591     plan tests => 24;
1592
1593     my $homebranch    = $builder->build( { source => 'Branch' } );
1594     my $holdingbranch = $builder->build( { source => 'Branch' } );
1595     my $otherbranch   = $builder->build( { source => 'Branch' } );
1596     my $patron_1      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1597     my $patron_2      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1598
1599     my $item = $builder->build_sample_item(
1600         {
1601             homebranch    => $homebranch->{branchcode},
1602             holdingbranch => $holdingbranch->{branchcode},
1603         }
1604     )->unblessed;
1605
1606     set_userenv($holdingbranch);
1607
1608     my $issue = AddIssue( $patron_1->unblessed, $item->{barcode} );
1609     is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
1610
1611     my ( $error, $question, $alerts );
1612
1613     # AllowReturnToBranch == anywhere
1614     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1615     ## Test that unknown barcodes don't generate internal server errors
1616     set_userenv($homebranch);
1617     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
1618     ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
1619     ## Can be issued from homebranch
1620     set_userenv($homebranch);
1621     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1622     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1623     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1624     ## Can be issued from holdingbranch
1625     set_userenv($holdingbranch);
1626     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1627     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1628     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1629     ## Can be issued from another branch
1630     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1631     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1632     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1633
1634     # AllowReturnToBranch == holdingbranch
1635     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1636     ## Cannot be issued from homebranch
1637     set_userenv($homebranch);
1638     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1639     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1640     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1641     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matched holdingbranch' );
1642     ## Can be issued from holdinbranch
1643     set_userenv($holdingbranch);
1644     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1645     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1646     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1647     ## Cannot be issued from another branch
1648     set_userenv($otherbranch);
1649     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1650     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1651     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1652     is( $error->{branch_to_return},         $holdingbranch->{branchcode}, 'branch_to_return matches holdingbranch' );
1653
1654     # AllowReturnToBranch == homebranch
1655     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1656     ## Can be issued from holdinbranch
1657     set_userenv($homebranch);
1658     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1659     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1660     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1661     ## Cannot be issued from holdinbranch
1662     set_userenv($holdingbranch);
1663     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1664     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1665     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1666     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1667     ## Cannot be issued from holdinbranch
1668     set_userenv($otherbranch);
1669     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1670     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1671     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1672     is( $error->{branch_to_return},         $homebranch->{branchcode}, 'branch_to_return matches homebranch' );
1673
1674     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1675 };
1676
1677 subtest 'AddIssue & AllowReturnToBranch' => sub {
1678     plan tests => 9;
1679
1680     my $homebranch    = $builder->build( { source => 'Branch' } );
1681     my $holdingbranch = $builder->build( { source => 'Branch' } );
1682     my $otherbranch   = $builder->build( { source => 'Branch' } );
1683     my $patron_1      = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1684     my $patron_2      = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1685
1686     my $item = $builder->build_sample_item(
1687         {
1688             homebranch    => $homebranch->{branchcode},
1689             holdingbranch => $holdingbranch->{branchcode},
1690         }
1691     )->unblessed;
1692
1693     set_userenv($holdingbranch);
1694
1695     my $ref_issue = 'Koha::Checkout';
1696     my $issue = AddIssue( $patron_1, $item->{barcode} );
1697
1698     my ( $error, $question, $alerts );
1699
1700     # AllowReturnToBranch == homebranch
1701     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1702     ## Can be issued from homebranch
1703     set_userenv($homebranch);
1704     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from homebranch');
1705     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1706     ## Can be issued from holdinbranch
1707     set_userenv($holdingbranch);
1708     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from holdingbranch');
1709     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1710     ## Can be issued from another branch
1711     set_userenv($otherbranch);
1712     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - anywhere | Can be issued from otherbranch');
1713     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1714
1715     # AllowReturnToBranch == holdinbranch
1716     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1717     ## Cannot be issued from homebranch
1718     set_userenv($homebranch);
1719     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from homebranch');
1720     ## Can be issued from holdingbranch
1721     set_userenv($holdingbranch);
1722     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - holdingbranch | Can be issued from holdingbranch');
1723     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1724     ## Cannot be issued from another branch
1725     set_userenv($otherbranch);
1726     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - holdingbranch | Cannot be issued from otherbranch');
1727
1728     # AllowReturnToBranch == homebranch
1729     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1730     ## Can be issued from homebranch
1731     set_userenv($homebranch);
1732     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue, 'AllowReturnToBranch - homebranch | Can be issued from homebranch' );
1733     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1734     ## Cannot be issued from holdinbranch
1735     set_userenv($holdingbranch);
1736     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from holdingbranch' );
1737     ## Cannot be issued from another branch
1738     set_userenv($otherbranch);
1739     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '', 'AllowReturnToBranch - homebranch | Cannot be issued from otherbranch' );
1740     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1741 };
1742
1743 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1744     plan tests => 8;
1745
1746     my $library = $builder->build( { source => 'Branch' } );
1747     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1748     my $item_1 = $builder->build_sample_item(
1749         {
1750             library => $library->{branchcode},
1751         }
1752     )->unblessed;
1753     my $item_2 = $builder->build_sample_item(
1754         {
1755             library => $library->{branchcode},
1756         }
1757     )->unblessed;
1758
1759     my ( $error, $question, $alerts );
1760
1761     # Patron cannot issue item_1, they have overdues
1762     my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1763     my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, $yesterday );    # Add an overdue
1764
1765     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1766     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1767     is( keys(%$error) + keys(%$alerts),  0, 'No key for error and alert' . str($error, $question, $alerts) );
1768     is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
1769
1770     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1771     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1772     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1773     is( $error->{USERBLOCKEDOVERDUE},      1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
1774
1775     # Patron cannot issue item_1, they are debarred
1776     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1777     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
1778     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1779     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1780     is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
1781
1782     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
1783     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1784     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1785     is( $error->{USERBLOCKEDNOENDDATE},    '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
1786 };
1787
1788 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
1789     plan tests => 1;
1790
1791     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1792     my $patron_category_x = $builder->build_object(
1793         {
1794             class => 'Koha::Patron::Categories',
1795             value => { category_type => 'X' }
1796         }
1797     );
1798     my $patron = $builder->build_object(
1799         {
1800             class => 'Koha::Patrons',
1801             value => {
1802                 categorycode  => $patron_category_x->categorycode,
1803                 gonenoaddress => undef,
1804                 lost          => undef,
1805                 debarred      => undef,
1806                 borrowernotes => ""
1807             }
1808         }
1809     );
1810     my $item_1 = $builder->build_sample_item(
1811         {
1812             library => $library->{branchcode},
1813         }
1814     )->unblessed;
1815
1816     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->{barcode} );
1817     is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
1818
1819     # TODO There are other tests to provide here
1820 };
1821
1822 subtest 'MultipleReserves' => sub {
1823     plan tests => 3;
1824
1825     my $biblio = $builder->build_sample_biblio();
1826
1827     my $branch = $library2->{branchcode};
1828
1829     my $item_1 = $builder->build_sample_item(
1830         {
1831             biblionumber     => $biblio->biblionumber,
1832             library          => $branch,
1833             replacementprice => 12.00,
1834             itype            => $itemtype,
1835         }
1836     );
1837
1838     my $item_2 = $builder->build_sample_item(
1839         {
1840             biblionumber     => $biblio->biblionumber,
1841             library          => $branch,
1842             replacementprice => 12.00,
1843             itype            => $itemtype,
1844         }
1845     );
1846
1847     my $bibitems       = '';
1848     my $priority       = '1';
1849     my $resdate        = undef;
1850     my $expdate        = undef;
1851     my $notes          = '';
1852     my $checkitem      = undef;
1853     my $found          = undef;
1854
1855     my %renewing_borrower_data = (
1856         firstname =>  'John',
1857         surname => 'Renewal',
1858         categorycode => $patron_category->{categorycode},
1859         branchcode => $branch,
1860     );
1861     my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
1862     my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
1863     my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
1864     my $datedue = dt_from_string( $issue->date_due() );
1865     is (defined $issue->date_due(), 1, "item 1 checked out");
1866     my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
1867
1868     my %reserving_borrower_data1 = (
1869         firstname =>  'Katrin',
1870         surname => 'Reservation',
1871         categorycode => $patron_category->{categorycode},
1872         branchcode => $branch,
1873     );
1874     my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
1875     AddReserve(
1876         {
1877             branchcode       => $branch,
1878             borrowernumber   => $reserving_borrowernumber1,
1879             biblionumber     => $biblio->biblionumber,
1880             priority         => $priority,
1881             reservation_date => $resdate,
1882             expiration_date  => $expdate,
1883             notes            => $notes,
1884             itemnumber       => $checkitem,
1885             found            => $found,
1886         }
1887     );
1888
1889     my %reserving_borrower_data2 = (
1890         firstname =>  'Kirk',
1891         surname => 'Reservation',
1892         categorycode => $patron_category->{categorycode},
1893         branchcode => $branch,
1894     );
1895     my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
1896     AddReserve(
1897         {
1898             branchcode       => $branch,
1899             borrowernumber   => $reserving_borrowernumber2,
1900             biblionumber     => $biblio->biblionumber,
1901             priority         => $priority,
1902             reservation_date => $resdate,
1903             expiration_date  => $expdate,
1904             notes            => $notes,
1905             itemnumber       => $checkitem,
1906             found            => $found,
1907         }
1908     );
1909
1910     {
1911         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1912         is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
1913     }
1914
1915     my $item_3 = $builder->build_sample_item(
1916         {
1917             biblionumber     => $biblio->biblionumber,
1918             library          => $branch,
1919             replacementprice => 12.00,
1920             itype            => $itemtype,
1921         }
1922     );
1923
1924     {
1925         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1926         is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
1927     }
1928 };
1929
1930 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
1931     plan tests => 5;
1932
1933     my $library = $builder->build( { source => 'Branch' } );
1934     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1935
1936     my $biblionumber = $builder->build_sample_biblio(
1937         {
1938             branchcode => $library->{branchcode},
1939         }
1940     )->biblionumber;
1941     my $item_1 = $builder->build_sample_item(
1942         {
1943             biblionumber => $biblionumber,
1944             library      => $library->{branchcode},
1945         }
1946     )->unblessed;
1947
1948     my $item_2 = $builder->build_sample_item(
1949         {
1950             biblionumber => $biblionumber,
1951             library      => $library->{branchcode},
1952         }
1953     )->unblessed;
1954
1955     my ( $error, $question, $alerts );
1956     my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, dt_from_string->add( days => 1 ) );
1957
1958     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1959     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1960     cmp_deeply(
1961         { error => $error, alerts => $alerts },
1962         { error => {}, alerts => {} },
1963         'No error or alert should be raised'
1964     );
1965     is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
1966
1967     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1968     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1969     cmp_deeply(
1970         { error => $error, question => $question, alerts => $alerts },
1971         { error => {}, question => {}, alerts => {} },
1972         'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1'
1973     );
1974
1975     # Add a subscription
1976     Koha::Subscription->new({ biblionumber => $biblionumber })->store;
1977
1978     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1979     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1980     cmp_deeply(
1981         { error => $error, question => $question, alerts => $alerts },
1982         { error => {}, question => {}, alerts => {} },
1983         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
1984     );
1985
1986     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1987     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1988     cmp_deeply(
1989         { error => $error, question => $question, alerts => $alerts },
1990         { error => {}, question => {}, alerts => {} },
1991         'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription'
1992     );
1993 };
1994
1995 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
1996     plan tests => 8;
1997
1998     my $library = $builder->build( { source => 'Branch' } );
1999     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2000
2001     # Add 2 items
2002     my $biblionumber = $builder->build_sample_biblio(
2003         {
2004             branchcode => $library->{branchcode},
2005         }
2006     )->biblionumber;
2007     my $item_1 = $builder->build_sample_item(
2008         {
2009             biblionumber => $biblionumber,
2010             library      => $library->{branchcode},
2011         }
2012     )->unblessed;
2013     my $item_2 = $builder->build_sample_item(
2014         {
2015             biblionumber => $biblionumber,
2016             library      => $library->{branchcode},
2017         }
2018     )->unblessed;
2019
2020     # And the circulation rule
2021     Koha::CirculationRules->search->delete;
2022     Koha::CirculationRules->set_rules(
2023         {
2024             categorycode => undef,
2025             itemtype     => undef,
2026             branchcode   => undef,
2027             rules        => {
2028                 issuelength => 1,
2029                 firstremind => 1,        # 1 day of grace
2030                 finedays    => 2,        # 2 days of fine per day of overdue
2031                 lengthunit  => 'days',
2032             }
2033         }
2034     );
2035
2036     # Patron cannot issue item_1, they have overdues
2037     my $now = dt_from_string;
2038     my $five_days_ago = $now->clone->subtract( days => 5 );
2039     my $ten_days_ago  = $now->clone->subtract( days => 10 );
2040     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
2041     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
2042       ;    # Add another overdue
2043
2044     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
2045     AddReturn( $item_1->{barcode}, $library->{branchcode}, undef, $now );
2046     my $debarments = Koha::Patron::Debarments::GetDebarments(
2047         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2048     is( scalar(@$debarments), 1 );
2049
2050     # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
2051     # Same for the others
2052     my $expected_expiration = output_pref(
2053         {
2054             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2055             dateformat => 'sql',
2056             dateonly   => 1
2057         }
2058     );
2059     is( $debarments->[0]->{expiration}, $expected_expiration );
2060
2061     AddReturn( $item_2->{barcode}, $library->{branchcode}, undef, $now );
2062     $debarments = Koha::Patron::Debarments::GetDebarments(
2063         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2064     is( scalar(@$debarments), 1 );
2065     $expected_expiration = output_pref(
2066         {
2067             dt         => $now->clone->add( days => ( 10 - 1 ) * 2 ),
2068             dateformat => 'sql',
2069             dateonly   => 1
2070         }
2071     );
2072     is( $debarments->[0]->{expiration}, $expected_expiration );
2073
2074     Koha::Patron::Debarments::DelUniqueDebarment(
2075         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2076
2077     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
2078     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
2079     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
2080       ;    # Add another overdue
2081     AddReturn( $item_1->{barcode}, $library->{branchcode}, undef, $now );
2082     $debarments = Koha::Patron::Debarments::GetDebarments(
2083         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2084     is( scalar(@$debarments), 1 );
2085     $expected_expiration = output_pref(
2086         {
2087             dt         => $now->clone->add( days => ( 5 - 1 ) * 2 ),
2088             dateformat => 'sql',
2089             dateonly   => 1
2090         }
2091     );
2092     is( $debarments->[0]->{expiration}, $expected_expiration );
2093
2094     AddReturn( $item_2->{barcode}, $library->{branchcode}, undef, $now );
2095     $debarments = Koha::Patron::Debarments::GetDebarments(
2096         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2097     is( scalar(@$debarments), 1 );
2098     $expected_expiration = output_pref(
2099         {
2100             dt => $now->clone->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
2101             dateformat => 'sql',
2102             dateonly   => 1
2103         }
2104     );
2105     is( $debarments->[0]->{expiration}, $expected_expiration );
2106 };
2107
2108 subtest 'AddReturn + suspension_chargeperiod' => sub {
2109     plan tests => 24;
2110
2111     my $library = $builder->build( { source => 'Branch' } );
2112     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2113
2114     my $biblionumber = $builder->build_sample_biblio(
2115         {
2116             branchcode => $library->{branchcode},
2117         }
2118     )->biblionumber;
2119     my $item_1 = $builder->build_sample_item(
2120         {
2121             biblionumber => $biblionumber,
2122             library      => $library->{branchcode},
2123         }
2124     )->unblessed;
2125
2126     # And the issuing rule
2127     Koha::CirculationRules->search->delete;
2128     Koha::CirculationRules->set_rules(
2129         {
2130             categorycode => '*',
2131             itemtype     => '*',
2132             branchcode   => '*',
2133             rules        => {
2134                 issuelength => 1,
2135                 firstremind => 0,    # 0 day of grace
2136                 finedays    => 2,    # 2 days of fine per day of overdue
2137                 suspension_chargeperiod => 1,
2138                 lengthunit              => 'days',
2139             }
2140         }
2141     );
2142
2143     my $now = dt_from_string;
2144     my $five_days_ago = $now->clone->subtract( days => 5 );
2145     # We want to charge 2 days every day, without grace
2146     # With 5 days of overdue: 5 * Z
2147     my $expected_expiration = $now->clone->add( days => ( 5 * 2 ) / 1 );
2148     test_debarment_on_checkout(
2149         {
2150             item            => $item_1,
2151             library         => $library,
2152             patron          => $patron,
2153             due_date        => $five_days_ago,
2154             expiration_date => $expected_expiration,
2155         }
2156     );
2157
2158     # We want to charge 2 days every 2 days, without grace
2159     # With 5 days of overdue: (5 * 2) / 2
2160     Koha::CirculationRules->set_rule(
2161         {
2162             categorycode => undef,
2163             branchcode   => undef,
2164             itemtype     => undef,
2165             rule_name    => 'suspension_chargeperiod',
2166             rule_value   => '2',
2167         }
2168     );
2169
2170     $expected_expiration = $now->clone->add( days => floor( 5 * 2 ) / 2 );
2171     test_debarment_on_checkout(
2172         {
2173             item            => $item_1,
2174             library         => $library,
2175             patron          => $patron,
2176             due_date        => $five_days_ago,
2177             expiration_date => $expected_expiration,
2178         }
2179     );
2180
2181     # We want to charge 2 days every 3 days, with 1 day of grace
2182     # With 5 days of overdue: ((5-1) / 3 ) * 2
2183     Koha::CirculationRules->set_rules(
2184         {
2185             categorycode => undef,
2186             branchcode   => undef,
2187             itemtype     => undef,
2188             rules        => {
2189                 suspension_chargeperiod => 3,
2190                 firstremind             => 1,
2191             }
2192         }
2193     );
2194     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
2195     test_debarment_on_checkout(
2196         {
2197             item            => $item_1,
2198             library         => $library,
2199             patron          => $patron,
2200             due_date        => $five_days_ago,
2201             expiration_date => $expected_expiration,
2202         }
2203     );
2204
2205     # Use finesCalendar to know if holiday must be skipped to calculate the due date
2206     # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
2207     Koha::CirculationRules->set_rules(
2208         {
2209             categorycode => undef,
2210             branchcode   => undef,
2211             itemtype     => undef,
2212             rules        => {
2213                 finedays                => 2,
2214                 suspension_chargeperiod => 1,
2215                 firstremind             => 0,
2216             }
2217         }
2218     );
2219     t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
2220     t::lib::Mocks::mock_preference('SuspensionsCalendar', 'noSuspensionsWhenClosed');
2221
2222     # Adding a holiday 2 days ago
2223     my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
2224     my $two_days_ago = $now->clone->subtract( days => 2 );
2225     $calendar->insert_single_holiday(
2226         day             => $two_days_ago->day,
2227         month           => $two_days_ago->month,
2228         year            => $two_days_ago->year,
2229         title           => 'holidayTest-2d',
2230         description     => 'holidayDesc 2 days ago'
2231     );
2232     # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
2233     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
2234     test_debarment_on_checkout(
2235         {
2236             item            => $item_1,
2237             library         => $library,
2238             patron          => $patron,
2239             due_date        => $five_days_ago,
2240             expiration_date => $expected_expiration,
2241         }
2242     );
2243
2244     # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
2245     my $two_days_ahead = $now->clone->add( days => 2 );
2246     $calendar->insert_single_holiday(
2247         day             => $two_days_ahead->day,
2248         month           => $two_days_ahead->month,
2249         year            => $two_days_ahead->year,
2250         title           => 'holidayTest+2d',
2251         description     => 'holidayDesc 2 days ahead'
2252     );
2253
2254     # Same as above, but we should skip D+2
2255     $expected_expiration = $now->clone->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
2256     test_debarment_on_checkout(
2257         {
2258             item            => $item_1,
2259             library         => $library,
2260             patron          => $patron,
2261             due_date        => $five_days_ago,
2262             expiration_date => $expected_expiration,
2263         }
2264     );
2265
2266     # Adding another holiday, day of expiration date
2267     my $expected_expiration_dt = dt_from_string($expected_expiration);
2268     $calendar->insert_single_holiday(
2269         day             => $expected_expiration_dt->day,
2270         month           => $expected_expiration_dt->month,
2271         year            => $expected_expiration_dt->year,
2272         title           => 'holidayTest_exp',
2273         description     => 'holidayDesc on expiration date'
2274     );
2275     # Expiration date will be the day after
2276     test_debarment_on_checkout(
2277         {
2278             item            => $item_1,
2279             library         => $library,
2280             patron          => $patron,
2281             due_date        => $five_days_ago,
2282             expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
2283         }
2284     );
2285
2286     test_debarment_on_checkout(
2287         {
2288             item            => $item_1,
2289             library         => $library,
2290             patron          => $patron,
2291             return_date     => $now->clone->add(days => 5),
2292             expiration_date => $now->clone->add(days => 5 + (5 * 2 - 1) ),
2293         }
2294     );
2295
2296     test_debarment_on_checkout(
2297         {
2298             item            => $item_1,
2299             library         => $library,
2300             patron          => $patron,
2301             due_date        => $now->clone->add(days => 1),
2302             return_date     => $now->clone->add(days => 5),
2303             expiration_date => $now->clone->add(days => 5 + (4 * 2 - 1) ),
2304         }
2305     );
2306
2307 };
2308
2309 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
2310     plan tests => 2;
2311
2312     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2313     my $patron1 = $builder->build_object(
2314         {
2315             class => 'Koha::Patrons',
2316             value => {
2317                 library      => $library->branchcode,
2318                 categorycode => $patron_category->{categorycode}
2319             }
2320         }
2321     );
2322     my $patron2 = $builder->build_object(
2323         {
2324             class => 'Koha::Patrons',
2325             value => {
2326                 library      => $library->branchcode,
2327                 categorycode => $patron_category->{categorycode}
2328             }
2329         }
2330     );
2331
2332     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
2333
2334     my $item = $builder->build_sample_item(
2335         {
2336             library      => $library->branchcode,
2337         }
2338     )->unblessed;
2339
2340     my ( $error, $question, $alerts );
2341     my $issue = AddIssue( $patron1->unblessed, $item->{barcode} );
2342
2343     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2344     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->{barcode} );
2345     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' );
2346
2347     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
2348     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->{barcode} );
2349     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' );
2350
2351     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
2352 };
2353
2354
2355 subtest 'AddReturn | is_overdue' => sub {
2356     plan tests => 8;
2357
2358     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'batchmod|moredetail|cronjob|additem|pendingreserves|onpayment');
2359     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
2360     t::lib::Mocks::mock_preference('finesMode', 'production');
2361     t::lib::Mocks::mock_preference('MaxFine', '100');
2362
2363     my $library = $builder->build( { source => 'Branch' } );
2364     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
2365     my $manager = $builder->build_object({ class => "Koha::Patrons" });
2366     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
2367
2368     my $item = $builder->build_sample_item(
2369         {
2370             library      => $library->{branchcode},
2371             replacementprice => 7
2372         }
2373     )->unblessed;
2374
2375     Koha::CirculationRules->search->delete;
2376     Koha::CirculationRules->set_rules(
2377         {
2378             categorycode => undef,
2379             itemtype     => undef,
2380             branchcode   => undef,
2381             rules        => {
2382                 issuelength  => 6,
2383                 lengthunit   => 'days',
2384                 fine         => 1,        # Charge 1 every day of overdue
2385                 chargeperiod => 1,
2386             }
2387         }
2388     );
2389
2390     my $now   = dt_from_string;
2391     my $one_day_ago   = $now->clone->subtract( days => 1 );
2392     my $two_days_ago  = $now->clone->subtract( days => 2 );
2393     my $five_days_ago = $now->clone->subtract( days => 5 );
2394     my $ten_days_ago  = $now->clone->subtract( days => 10 );
2395     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
2396
2397     # No return date specified, today will be used => 10 days overdue charged
2398     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2399     AddReturn( $item->{barcode}, $library->{branchcode} );
2400     is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
2401     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2402
2403     # specify return date 5 days before => no overdue charged
2404     AddIssue( $patron->unblessed, $item->{barcode}, $five_days_ago ); # date due was 5d ago
2405     AddReturn( $item->{barcode}, $library->{branchcode}, undef, $ten_days_ago );
2406     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2407     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2408
2409     # specify return date 5 days later => 5 days overdue charged
2410     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2411     AddReturn( $item->{barcode}, $library->{branchcode}, undef, $five_days_ago );
2412     is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
2413     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2414
2415     # specify return date 5 days later, specify exemptfine => no overdue charge
2416     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2417     AddReturn( $item->{barcode}, $library->{branchcode}, 1, $five_days_ago );
2418     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2419     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2420
2421     subtest 'bug 22877' => sub {
2422
2423         plan tests => 3;
2424
2425         my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago );    # date due was 10d ago
2426
2427         # Fake fines cronjob on this checkout
2428         my ($fine) =
2429           CalcFine( $item, $patron->categorycode, $library->{branchcode},
2430             $ten_days_ago, $now );
2431         UpdateFine(
2432             {
2433                 issue_id       => $issue->issue_id,
2434                 itemnumber     => $item->{itemnumber},
2435                 borrowernumber => $patron->borrowernumber,
2436                 amount         => $fine,
2437                 due            => output_pref($ten_days_ago)
2438             }
2439         );
2440         is( int( $patron->account->balance() ),
2441             10, "Overdue fine of 10 days overdue" );
2442
2443         # Fake longoverdue with charge and not marking returned
2444         LostItem( $item->{itemnumber}, 'cronjob', 0 );
2445         is( int( $patron->account->balance() ),
2446             17, "Lost fine of 7 plus 10 days overdue" );
2447
2448         # Now we return it today
2449         AddReturn( $item->{barcode}, $library->{branchcode} );
2450         is( int( $patron->account->balance() ),
2451             17, "Should have a single 10 days overdue fine and lost charge" );
2452
2453         # Cleanup
2454         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2455     };
2456
2457     subtest 'bug 8338 | backdated return resulting in zero amount fine' => sub {
2458
2459         plan tests => 17;
2460
2461         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2462
2463         my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $one_day_ago );    # date due was 1d ago
2464
2465         # Fake fines cronjob on this checkout
2466         my ($fine) =
2467           CalcFine( $item, $patron->categorycode, $library->{branchcode},
2468             $one_day_ago, $now );
2469         UpdateFine(
2470             {
2471                 issue_id       => $issue->issue_id,
2472                 itemnumber     => $item->{itemnumber},
2473                 borrowernumber => $patron->borrowernumber,
2474                 amount         => $fine,
2475                 due            => output_pref($one_day_ago)
2476             }
2477         );
2478         is( int( $patron->account->balance() ),
2479             1, "Overdue fine of 1 day overdue" );
2480
2481         # Backdated return (dropbox mode example - charge should be removed)
2482         AddReturn( $item->{barcode}, $library->{branchcode}, 1, $one_day_ago );
2483         is( int( $patron->account->balance() ),
2484             0, "Overdue fine should be annulled" );
2485         my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2486         is( $lines->count, 0, "Overdue fine accountline has been removed");
2487
2488         $issue = AddIssue( $patron->unblessed, $item->{barcode}, $two_days_ago );    # date due was 2d ago
2489
2490         # Fake fines cronjob on this checkout
2491         ($fine) =
2492           CalcFine( $item, $patron->categorycode, $library->{branchcode},
2493             $two_days_ago, $now );
2494         UpdateFine(
2495             {
2496                 issue_id       => $issue->issue_id,
2497                 itemnumber     => $item->{itemnumber},
2498                 borrowernumber => $patron->borrowernumber,
2499                 amount         => $fine,
2500                 due            => output_pref($one_day_ago)
2501             }
2502         );
2503         is( int( $patron->account->balance() ),
2504             2, "Overdue fine of 2 days overdue" );
2505
2506         # Payment made against fine
2507         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2508         my $debit = $lines->next;
2509         my $credit = $patron->account->add_credit(
2510             {
2511                 amount    => 2,
2512                 type      => 'PAYMENT',
2513                 interface => 'test',
2514             }
2515         );
2516         $credit->apply(
2517             { debits => [ $debit ], offset_type => 'Payment' } );
2518
2519         is( int( $patron->account->balance() ),
2520             0, "Overdue fine should be paid off" );
2521         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber });
2522         is ( $lines->count, 2, "Overdue (debit) and Payment (credit) present");
2523         my $line = $lines->next;
2524         is( $line->amount+0, 2, "Overdue fine amount remains as 2 days");
2525         is( $line->amountoutstanding+0, 0, "Overdue fine amountoutstanding reduced to 0");
2526
2527         # Backdated return (dropbox mode example - charge should be removed)
2528         AddReturn( $item->{barcode}, $library->{branchcode}, undef, $one_day_ago );
2529         is( int( $patron->account->balance() ),
2530             -1, "Refund credit has been applied" );
2531         $lines = Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber }, { order_by => { '-asc' => 'accountlines_id' }});
2532         is( $lines->count, 3, "Overdue (debit), Payment (credit) and Refund (credit) are all present");
2533
2534         $line = $lines->next;
2535         is($line->amount+0,1, "Overdue fine amount has been reduced to 1");
2536         is($line->amountoutstanding+0,0, "Overdue fine amount outstanding remains at 0");
2537         is($line->status,'RETURNED', "Overdue fine is fixed");
2538         $line = $lines->next;
2539         is($line->amount+0,-2, "Original payment amount remains as 2");
2540         is($line->amountoutstanding+0,0, "Original payment remains applied");
2541         $line = $lines->next;
2542         is($line->amount+0,-1, "Refund amount correctly set to 1");
2543         is($line->amountoutstanding+0,-1, "Refund amount outstanding unspent");
2544
2545         # Cleanup
2546         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2547     };
2548
2549     subtest 'bug 25417 | backdated return + exemptfine' => sub {
2550
2551         plan tests => 2;
2552
2553         t::lib::Mocks::mock_preference('CalculateFinesOnBackdate', 1);
2554
2555         my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $one_day_ago );    # date due was 1d ago
2556
2557         # Fake fines cronjob on this checkout
2558         my ($fine) =
2559           CalcFine( $item, $patron->categorycode, $library->{branchcode},
2560             $one_day_ago, $now );
2561         UpdateFine(
2562             {
2563                 issue_id       => $issue->issue_id,
2564                 itemnumber     => $item->{itemnumber},
2565                 borrowernumber => $patron->borrowernumber,
2566                 amount         => $fine,
2567                 due            => output_pref($one_day_ago)
2568             }
2569         );
2570         is( int( $patron->account->balance() ),
2571             1, "Overdue fine of 1 day overdue" );
2572
2573         # Backdated return (dropbox mode example - charge should no longer exist)
2574         AddReturn( $item->{barcode}, $library->{branchcode}, 1, $one_day_ago );
2575         is( int( $patron->account->balance() ),
2576             0, "Overdue fine should be annulled" );
2577
2578         # Cleanup
2579         Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2580     };
2581
2582     subtest 'bug 24075 | backdated return with return datetime matching due datetime' => sub {
2583         plan tests => 7;
2584
2585         t::lib::Mocks::mock_preference( 'CalculateFinesOnBackdate', 1 );
2586
2587         my $due_date = dt_from_string;
2588         my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $due_date );
2589
2590         # Add fine
2591         UpdateFine(
2592             {
2593                 issue_id       => $issue->issue_id,
2594                 itemnumber     => $item->{itemnumber},
2595                 borrowernumber => $patron->borrowernumber,
2596                 amount         => 0.25,
2597                 due            => output_pref($due_date)
2598             }
2599         );
2600         is( $patron->account->balance(),
2601             0.25, 'Overdue fine of $0.25 recorded' );
2602
2603         # Backdate return to exact due date and time
2604         my ( undef, $message ) =
2605           AddReturn( $item->{barcode}, $library->{branchcode},
2606             undef, $due_date );
2607
2608         my $accountline =
2609           Koha::Account::Lines->find( { issue_id => $issue->id } );
2610         ok( !$accountline, 'accountline removed as expected' );
2611
2612         # Re-issue
2613         $issue = AddIssue( $patron->unblessed, $item->{barcode}, $due_date );
2614
2615         # Add fine
2616         UpdateFine(
2617             {
2618                 issue_id       => $issue->issue_id,
2619                 itemnumber     => $item->{itemnumber},
2620                 borrowernumber => $patron->borrowernumber,
2621                 amount         => .25,
2622                 due            => output_pref($due_date)
2623             }
2624         );
2625         is( $patron->account->balance(),
2626             0.25, 'Overdue fine of $0.25 recorded' );
2627
2628         # Partial pay accruing fine
2629         my $lines = Koha::Account::Lines->search(
2630             {
2631                 borrowernumber => $patron->borrowernumber,
2632                 issue_id       => $issue->id
2633             }
2634         );
2635         my $debit  = $lines->next;
2636         my $credit = $patron->account->add_credit(
2637             {
2638                 amount    => .20,
2639                 type      => 'PAYMENT',
2640                 interface => 'test',
2641             }
2642         );
2643         $credit->apply( { debits => [$debit], offset_type => 'Payment' } );
2644
2645         is( $patron->account->balance(), .05, 'Overdue fine reduced to $0.05' );
2646
2647         # Backdate return to exact due date and time
2648         ( undef, $message ) =
2649           AddReturn( $item->{barcode}, $library->{branchcode},
2650             undef, $due_date );
2651
2652         $lines = Koha::Account::Lines->search(
2653             {
2654                 borrowernumber => $patron->borrowernumber,
2655                 issue_id       => $issue->id
2656             }
2657         );
2658         $accountline = $lines->next;
2659         is( $accountline->amountoutstanding + 0,
2660             0, 'Partially paid fee amount outstanding was reduced to 0' );
2661         is( $accountline->amount + 0,
2662             0, 'Partially paid fee amount was reduced to 0' );
2663         is( $patron->account->balance(), -0.20, 'Patron refund recorded' );
2664
2665         # Cleanup
2666         Koha::Account::Lines->search(
2667             { borrowernumber => $patron->borrowernumber } )->delete;
2668     };
2669 };
2670
2671 subtest '_FixAccountForLostAndFound' => sub {
2672
2673     plan tests => 5;
2674
2675     t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
2676     t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
2677
2678     my $processfee_amount  = 20;
2679     my $replacement_amount = 99.00;
2680     my $item_type          = $builder->build_object(
2681         {   class => 'Koha::ItemTypes',
2682             value => {
2683                 notforloan         => undef,
2684                 rentalcharge       => 0,
2685                 defaultreplacecost => undef,
2686                 processfee         => $processfee_amount,
2687                 rentalcharge_daily => 0,
2688             }
2689         }
2690     );
2691     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2692
2693     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Daria' });
2694
2695     subtest 'Full write-off tests' => sub {
2696
2697         plan tests => 12;
2698
2699         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2700         my $manager = $builder->build_object({ class => "Koha::Patrons" });
2701         t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
2702
2703         my $item = $builder->build_sample_item(
2704             {
2705                 biblionumber     => $biblio->biblionumber,
2706                 library          => $library->branchcode,
2707                 replacementprice => $replacement_amount,
2708                 itype            => $item_type->itemtype,
2709             }
2710         );
2711
2712         AddIssue( $patron->unblessed, $item->barcode );
2713
2714         # Simulate item marked as lost
2715         $item->itemlost(3)->store;
2716         LostItem( $item->itemnumber, 1 );
2717
2718         my $processing_fee_lines = Koha::Account::Lines->search(
2719             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2720         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2721         my $processing_fee_line = $processing_fee_lines->next;
2722         is( $processing_fee_line->amount + 0,
2723             $processfee_amount, 'The right PROCESSING amount is generated' );
2724         is( $processing_fee_line->amountoutstanding + 0,
2725             $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2726
2727         my $lost_fee_lines = Koha::Account::Lines->search(
2728             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2729         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2730         my $lost_fee_line = $lost_fee_lines->next;
2731         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2732         is( $lost_fee_line->amountoutstanding + 0,
2733             $replacement_amount, 'The right LOST amountoutstanding is generated' );
2734         is( $lost_fee_line->status,
2735             undef, 'The LOST status was not set' );
2736
2737         my $account = $patron->account;
2738         my $debts   = $account->outstanding_debits;
2739
2740         # Write off the debt
2741         my $credit = $account->add_credit(
2742             {   amount => $account->balance,
2743                 type   => 'WRITEOFF',
2744                 interface => 'test',
2745             }
2746         );
2747         $credit->apply( { debits => [ $debts->as_list ], offset_type => 'Writeoff' } );
2748
2749         my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2750         is( $credit_return_id, undef, 'No LOST_FOUND account line added' );
2751
2752         $lost_fee_line->discard_changes; # reload from DB
2753         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2754         is( $lost_fee_line->debit_type_code,
2755             'LOST', 'Lost fee now still has account type of LOST' );
2756         is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
2757
2758         is( $patron->account->balance, -0, 'The patron balance is 0, everything was written off' );
2759     };
2760
2761     subtest 'Full payment tests' => sub {
2762
2763         plan tests => 13;
2764
2765         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2766
2767         my $item = $builder->build_sample_item(
2768             {
2769                 biblionumber     => $biblio->biblionumber,
2770                 library          => $library->branchcode,
2771                 replacementprice => $replacement_amount,
2772                 itype            => $item_type->itemtype
2773             }
2774         );
2775
2776         AddIssue( $patron->unblessed, $item->barcode );
2777
2778         # Simulate item marked as lost
2779         $item->itemlost(1)->store;
2780         LostItem( $item->itemnumber, 1 );
2781
2782         my $processing_fee_lines = Koha::Account::Lines->search(
2783             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2784         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2785         my $processing_fee_line = $processing_fee_lines->next;
2786         is( $processing_fee_line->amount + 0,
2787             $processfee_amount, 'The right PROCESSING amount is generated' );
2788         is( $processing_fee_line->amountoutstanding + 0,
2789             $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2790
2791         my $lost_fee_lines = Koha::Account::Lines->search(
2792             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2793         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2794         my $lost_fee_line = $lost_fee_lines->next;
2795         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2796         is( $lost_fee_line->amountoutstanding + 0,
2797             $replacement_amount, 'The right LOST amountountstanding is generated' );
2798
2799         my $account = $patron->account;
2800         my $debts   = $account->outstanding_debits;
2801
2802         # Write off the debt
2803         my $credit = $account->add_credit(
2804             {   amount => $account->balance,
2805                 type   => 'PAYMENT',
2806                 interface => 'test',
2807             }
2808         );
2809         $credit->apply( { debits => [ $debts->as_list ], offset_type => 'Payment' } );
2810
2811         my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2812         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2813
2814         is( $credit_return->credit_type_code, 'LOST_FOUND', 'An account line of type LOST_FOUND is added' );
2815         is( $credit_return->amount + 0,
2816             -99.00, 'The account line of type LOST_FOUND has an amount of -99' );
2817         is( $credit_return->amountoutstanding + 0,
2818             -99.00, 'The account line of type LOST_FOUND has an amountoutstanding of -99' );
2819
2820         $lost_fee_line->discard_changes;
2821         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2822         is( $lost_fee_line->debit_type_code,
2823             'LOST', 'Lost fee now still has account type of LOST' );
2824         is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
2825
2826         is( $patron->account->balance,
2827             -99, 'The patron balance is -99, a credit that equals the lost fee payment' );
2828     };
2829
2830     subtest 'Test without payment or write off' => sub {
2831
2832         plan tests => 13;
2833
2834         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2835
2836         my $item = $builder->build_sample_item(
2837             {
2838                 biblionumber     => $biblio->biblionumber,
2839                 library          => $library->branchcode,
2840                 replacementprice => 23.00,
2841                 replacementprice => $replacement_amount,
2842                 itype            => $item_type->itemtype
2843             }
2844         );
2845
2846         AddIssue( $patron->unblessed, $item->barcode );
2847
2848         # Simulate item marked as lost
2849         $item->itemlost(3)->store;
2850         LostItem( $item->itemnumber, 1 );
2851
2852         my $processing_fee_lines = Koha::Account::Lines->search(
2853             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2854         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2855         my $processing_fee_line = $processing_fee_lines->next;
2856         is( $processing_fee_line->amount + 0,
2857             $processfee_amount, 'The right PROCESSING amount is generated' );
2858         is( $processing_fee_line->amountoutstanding + 0,
2859             $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2860
2861         my $lost_fee_lines = Koha::Account::Lines->search(
2862             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2863         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2864         my $lost_fee_line = $lost_fee_lines->next;
2865         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2866         is( $lost_fee_line->amountoutstanding + 0,
2867             $replacement_amount, 'The right LOST amountountstanding is generated' );
2868
2869         my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2870         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2871
2872         is( $credit_return->credit_type_code, 'LOST_FOUND', 'An account line of type LOST_FOUND is added' );
2873         is( $credit_return->amount + 0, -99.00, 'The account line of type LOST_FOUND has an amount of -99' );
2874         is( $credit_return->amountoutstanding + 0, 0, 'The account line of type LOST_FOUND has an amountoutstanding of 0' );
2875
2876         $lost_fee_line->discard_changes;
2877         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2878         is( $lost_fee_line->debit_type_code,
2879             'LOST', 'Lost fee now still has account type of LOST' );
2880         is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
2881
2882         is( $patron->account->balance, 20, 'The patron balance is 20, still owes the processing fee' );
2883     };
2884
2885     subtest 'Test with partial payement and write off, and remaining debt' => sub {
2886
2887         plan tests => 16;
2888
2889         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2890         my $item = $builder->build_sample_item(
2891             {
2892                 biblionumber     => $biblio->biblionumber,
2893                 library          => $library->branchcode,
2894                 replacementprice => $replacement_amount,
2895                 itype            => $item_type->itemtype
2896             }
2897         );
2898
2899         AddIssue( $patron->unblessed, $item->barcode );
2900
2901         # Simulate item marked as lost
2902         $item->itemlost(1)->store;
2903         LostItem( $item->itemnumber, 1 );
2904
2905         my $processing_fee_lines = Koha::Account::Lines->search(
2906             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'PROCESSING' } );
2907         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2908         my $processing_fee_line = $processing_fee_lines->next;
2909         is( $processing_fee_line->amount + 0,
2910             $processfee_amount, 'The right PROCESSING amount is generated' );
2911         is( $processing_fee_line->amountoutstanding + 0,
2912             $processfee_amount, 'The right PROCESSING amountoutstanding is generated' );
2913
2914         my $lost_fee_lines = Koha::Account::Lines->search(
2915             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
2916         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2917         my $lost_fee_line = $lost_fee_lines->next;
2918         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
2919         is( $lost_fee_line->amountoutstanding + 0,
2920             $replacement_amount, 'The right LOST amountountstanding is generated' );
2921
2922         my $account = $patron->account;
2923         is( $account->balance, $processfee_amount + $replacement_amount, 'Balance is PROCESSING + L' );
2924
2925         # Partially pay fee
2926         my $payment_amount = 27;
2927         my $payment        = $account->add_credit(
2928             {   amount => $payment_amount,
2929                 type   => 'PAYMENT',
2930                 interface => 'test',
2931             }
2932         );
2933
2934         $payment->apply( { debits => [ $lost_fee_line ], offset_type => 'Payment' } );
2935
2936         # Partially write off fee
2937         my $write_off_amount = 25;
2938         my $write_off        = $account->add_credit(
2939             {   amount => $write_off_amount,
2940                 type   => 'WRITEOFF',
2941                 interface => 'test',
2942             }
2943         );
2944         $write_off->apply( { debits => [ $lost_fee_line ], offset_type => 'Writeoff' } );
2945
2946         is( $account->balance,
2947             $processfee_amount + $replacement_amount - $payment_amount - $write_off_amount,
2948             'Payment and write off applied'
2949         );
2950
2951         # Store the amountoutstanding value
2952         $lost_fee_line->discard_changes;
2953         my $outstanding = $lost_fee_line->amountoutstanding;
2954
2955         my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
2956         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2957
2958         is( $account->balance, $processfee_amount - $payment_amount, 'Balance is PROCESSING - PAYMENT (LOST_FOUND)' );
2959
2960         $lost_fee_line->discard_changes;
2961         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2962         is( $lost_fee_line->debit_type_code,
2963             'LOST', 'Lost fee now still has account type of LOST' );
2964         is( $lost_fee_line->status, 'FOUND', "Lost fee now has account status of FOUND");
2965
2966         is( $credit_return->credit_type_code, 'LOST_FOUND', 'An account line of type LOST_FOUND is added' );
2967         is( $credit_return->amount + 0,
2968             ($payment_amount + $outstanding ) * -1,
2969             'The account line of type LOST_FOUND has an amount equal to the payment + outstanding'
2970         );
2971         is( $credit_return->amountoutstanding + 0,
2972             $payment_amount * -1,
2973             'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
2974         );
2975
2976         is( $account->balance,
2977             $processfee_amount - $payment_amount,
2978             'The patron balance is the difference between the PROCESSING and the credit'
2979         );
2980     };
2981
2982     subtest 'Partial payement, existing debits and AccountAutoReconcile' => sub {
2983
2984         plan tests => 8;
2985
2986         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2987         my $barcode = 'KD123456793';
2988         my $replacement_amount = 100;
2989         my $processfee_amount  = 20;
2990
2991         my $item_type          = $builder->build_object(
2992             {   class => 'Koha::ItemTypes',
2993                 value => {
2994                     notforloan         => undef,
2995                     rentalcharge       => 0,
2996                     defaultreplacecost => undef,
2997                     processfee         => 0,
2998                     rentalcharge_daily => 0,
2999                 }
3000             }
3001         );
3002         my $item = Koha::Item->new(
3003             {
3004                 biblionumber     => $biblio->biblionumber,
3005                 homebranch       => $library->branchcode,
3006                 holdingbranch    => $library->branchcode,
3007                 barcode          => $barcode,
3008                 replacementprice => $replacement_amount,
3009                 itype            => $item_type->itemtype
3010             },
3011         )->store;
3012
3013         AddIssue( $patron->unblessed, $barcode );
3014
3015         # Simulate item marked as lost
3016         $item->itemlost(1)->store;
3017         LostItem( $item->itemnumber, 1 );
3018
3019         my $lost_fee_lines = Koha::Account::Lines->search(
3020             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, debit_type_code => 'LOST' } );
3021         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
3022         my $lost_fee_line = $lost_fee_lines->next;
3023         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right LOST amount is generated' );
3024         is( $lost_fee_line->amountoutstanding + 0,
3025             $replacement_amount, 'The right LOST amountountstanding is generated' );
3026
3027         my $account = $patron->account;
3028         is( $account->balance, $replacement_amount, 'Balance is L' );
3029
3030         # Partially pay fee
3031         my $payment_amount = 27;
3032         my $payment        = $account->add_credit(
3033             {   amount => $payment_amount,
3034                 type   => 'PAYMENT',
3035                 interface => 'test',
3036             }
3037         );
3038         $payment->apply({ debits => [ $lost_fee_line ], offset_type => 'Payment' });
3039
3040         is( $account->balance,
3041             $replacement_amount - $payment_amount,
3042             'Payment applied'
3043         );
3044
3045         my $manual_debit_amount = 80;
3046         $account->add_debit( { amount => $manual_debit_amount, type => 'OVERDUE', interface =>'test' } );
3047
3048         is( $account->balance, $manual_debit_amount + $replacement_amount - $payment_amount, 'Manual debit applied' );
3049
3050         t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
3051
3052         my $credit_return_id = C4::Circulation::_FixAccountForLostAndFound( $item->itemnumber, $patron->id );
3053         my $credit_return = Koha::Account::Lines->find($credit_return_id);
3054
3055         is( $account->balance, $manual_debit_amount - $payment_amount, 'Balance is PROCESSING - payment (LOST_FOUND)' );
3056
3057         my $manual_debit = Koha::Account::Lines->search({ borrowernumber => $patron->id, debit_type_code => 'OVERDUE', status => 'UNRETURNED' })->next;
3058         is( $manual_debit->amountoutstanding + 0, $manual_debit_amount - $payment_amount, 'reconcile_balance was called' );
3059     };
3060 };
3061
3062 subtest '_FixOverduesOnReturn' => sub {
3063     plan tests => 14;
3064
3065     my $manager = $builder->build_object({ class => "Koha::Patrons" });
3066     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3067
3068     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
3069
3070     my $branchcode  = $library2->{branchcode};
3071
3072     my $item = $builder->build_sample_item(
3073         {
3074             biblionumber     => $biblio->biblionumber,
3075             library          => $branchcode,
3076             replacementprice => 99.00,
3077             itype            => $itemtype,
3078         }
3079     );
3080
3081     my $patron = $builder->build( { source => 'Borrower' } );
3082
3083     ## Start with basic call, should just close out the open fine
3084     my $accountline = Koha::Account::Line->new(
3085         {
3086             borrowernumber => $patron->{borrowernumber},
3087             debit_type_code    => 'OVERDUE',
3088             status         => 'UNRETURNED',
3089             itemnumber     => $item->itemnumber,
3090             amount => 99.00,
3091             amountoutstanding => 99.00,
3092             interface => 'test',
3093         }
3094     )->store();
3095
3096     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, undef, 'RETURNED' );
3097
3098     $accountline->_result()->discard_changes();
3099
3100     is( $accountline->amountoutstanding+0, 99, 'Fine has the same amount outstanding as previously' );
3101     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3102     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3103
3104     ## Run again, with exemptfine enabled
3105     $accountline->set(
3106         {
3107             debit_type_code    => 'OVERDUE',
3108             status         => 'UNRETURNED',
3109             amountoutstanding => 99.00,
3110         }
3111     )->store();
3112
3113     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3114
3115     $accountline->_result()->discard_changes();
3116     my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3117
3118     is( $accountline->amountoutstanding + 0, 0, 'Fine amountoutstanding has been reduced to 0' );
3119     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3120     is( $accountline->status, 'FORGIVEN', 'Open fine ( account type OVERDUE ) has been set to fine forgiven ( status FORGIVEN )');
3121     is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
3122     is( $offset->amount + 0, -99, "Amount of offset is correct" );
3123     my $credit = $offset->credit;
3124     is( ref $credit, "Koha::Account::Line", "Found matching credit for fine forgiveness" );
3125     is( $credit->amount + 0, -99, "Credit amount is set correctly" );
3126     is( $credit->amountoutstanding + 0, 0, "Credit amountoutstanding is correctly set to 0" );
3127
3128     # Bug 25417 - Only forgive fines where there is an amount outstanding to forgive
3129     $accountline->set(
3130         {
3131             debit_type_code    => 'OVERDUE',
3132             status         => 'UNRETURNED',
3133             amountoutstanding => 0.00,
3134         }
3135     )->store();
3136     $offset->delete;
3137
3138     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1, 'RETURNED' );
3139
3140     $accountline->_result()->discard_changes();
3141     $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
3142     is( $offset, undef, "No offset created when trying to forgive fine with no outstanding balance" );
3143     isnt( $accountline->status, 'UNRETURNED', 'Open fine ( account type OVERDUE ) has been closed out ( status not UNRETURNED )');
3144     is( $accountline->status, 'RETURNED', 'Passed status has been used to set as RETURNED )');
3145 };
3146
3147 subtest '_FixAccountForLostAndFound returns undef if patron is deleted' => sub {
3148     plan tests => 1;
3149
3150     my $manager = $builder->build_object({ class => "Koha::Patrons" });
3151     t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
3152
3153     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
3154
3155     my $branchcode  = $library2->{branchcode};
3156
3157     my $item = $builder->build_sample_item(
3158         {
3159             biblionumber     => $biblio->biblionumber,
3160             library          => $branchcode,
3161             replacementprice => 99.00,
3162             itype            => $itemtype,
3163         }
3164     );
3165
3166     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
3167
3168     ## Start with basic call, should just close out the open fine
3169     my $accountline = Koha::Account::Line->new(
3170         {
3171             borrowernumber => $patron->id,
3172             debit_type_code    => 'LOST',
3173             status         => undef,
3174             itemnumber     => $item->itemnumber,
3175             amount => 99.00,
3176             amountoutstanding => 99.00,
3177             interface => 'test',
3178         }
3179     )->store();
3180
3181     $patron->delete();
3182
3183     my $return_value = C4::Circulation::_FixAccountForLostAndFound( $patron->id, $item->itemnumber );
3184
3185     is( $return_value, undef, "_FixAccountForLostAndFound returns undef if patron is deleted" );
3186
3187 };
3188
3189 subtest 'Set waiting flag' => sub {
3190     plan tests => 11;
3191
3192     my $library_1 = $builder->build( { source => 'Branch' } );
3193     my $patron_1  = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3194     my $library_2 = $builder->build( { source => 'Branch' } );
3195     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3196
3197     my $item = $builder->build_sample_item(
3198         {
3199             library      => $library_1->{branchcode},
3200         }
3201     )->unblessed;
3202
3203     set_userenv( $library_2 );
3204     my $reserve_id = AddReserve(
3205         {
3206             branchcode     => $library_2->{branchcode},
3207             borrowernumber => $patron_2->{borrowernumber},
3208             biblionumber   => $item->{biblionumber},
3209             priority       => 1,
3210             itemnumber     => $item->{itemnumber},
3211         }
3212     );
3213
3214     set_userenv( $library_1 );
3215     my $do_transfer = 1;
3216     my ( $res, $rr ) = AddReturn( $item->{barcode}, $library_1->{branchcode} );
3217     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
3218     my $hold = Koha::Holds->find( $reserve_id );
3219     is( $hold->found, 'T', 'Hold is in transit' );
3220
3221     my ( $status ) = CheckReserves($item->{itemnumber});
3222     is( $status, 'Reserved', 'Hold is not waiting yet');
3223
3224     set_userenv( $library_2 );
3225     $do_transfer = 0;
3226     AddReturn( $item->{barcode}, $library_2->{branchcode} );
3227     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
3228     $hold = Koha::Holds->find( $reserve_id );
3229     is( $hold->found, 'W', 'Hold is waiting' );
3230     ( $status ) = CheckReserves($item->{itemnumber});
3231     is( $status, 'Waiting', 'Now the hold is waiting');
3232
3233     #Bug 21944 - Waiting transfer checked in at branch other than pickup location
3234     set_userenv( $library_1 );
3235     (undef, my $messages, undef, undef ) = AddReturn ( $item->{barcode}, $library_1->{branchcode} );
3236     $hold = Koha::Holds->find( $reserve_id );
3237     is( $hold->found, undef, 'Hold is no longer marked waiting' );
3238     is( $hold->priority, 1,  "Hold is now priority one again");
3239     is( $hold->waitingdate, undef, "Hold no longer has a waiting date");
3240     is( $hold->itemnumber, $item->{itemnumber}, "Hold has retained its' itemnumber");
3241     is( $messages->{ResFound}->{ResFound}, "Reserved", "Hold is still returned");
3242     is( $messages->{ResFound}->{found}, undef, "Hold is no longer marked found in return message");
3243     is( $messages->{ResFound}->{priority}, 1, "Hold is priority 1 in return message");
3244 };
3245
3246 subtest 'Cancel transfers on lost items' => sub {
3247     plan tests => 5;
3248     my $library_1 = $builder->build( { source => 'Branch' } );
3249     my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3250     my $library_2 = $builder->build( { source => 'Branch' } );
3251     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
3252     my $biblio = $builder->build_sample_biblio({branchcode => $library->{branchcode}});
3253     my $item   = $builder->build_sample_item({
3254         biblionumber  => $biblio->biblionumber,
3255         library    => $library_1->{branchcode},
3256     });
3257
3258     set_userenv( $library_2 );
3259     my $reserve_id = AddReserve(
3260         {
3261             branchcode     => $library_2->{branchcode},
3262             borrowernumber => $patron_2->{borrowernumber},
3263             biblionumber   => $item->biblionumber,
3264             priority       => 1,
3265             itemnumber     => $item->itemnumber,
3266         }
3267     );
3268
3269     #Return book and add transfer
3270     set_userenv( $library_1 );
3271     my $do_transfer = 1;
3272     my ( $res, $rr ) = AddReturn( $item->barcode, $library_1->{branchcode} );
3273     ModReserveAffect( $item->itemnumber, undef, $do_transfer, $reserve_id );
3274     C4::Circulation::transferbook( $library_2->{branchcode}, $item->barcode );
3275     my $hold = Koha::Holds->find( $reserve_id );
3276     is( $hold->found, 'T', 'Hold is in transit' );
3277
3278     #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
3279     my ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3280     is( $tobranch, $library_2->{branchcode}, 'The transfer record exists in the branchtransfers table');
3281     my $itemcheck = Koha::Items->find($item->itemnumber);
3282     is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Items holding branch is the transfers origin branch before it is marked as lost' );
3283
3284     #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
3285     $item->itemlost(1)->store;
3286     LostItem( $item->itemnumber, 'test', 1 );
3287     ($datesent,$frombranch,$tobranch) = GetTransfers($item->itemnumber);
3288     is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
3289     $itemcheck = Koha::Items->find($item->itemnumber);
3290     is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
3291 };
3292
3293 subtest 'CanBookBeIssued | is_overdue' => sub {
3294     plan tests => 3;
3295
3296     # Set a simple circ policy
3297     Koha::CirculationRules->set_rules(
3298         {
3299             categorycode => undef,
3300             branchcode   => undef,
3301             itemtype     => undef,
3302             rules        => {
3303                 maxissueqty     => 1,
3304                 reservesallowed => 25,
3305                 issuelength     => 14,
3306                 lengthunit      => 'days',
3307                 renewalsallowed => 1,
3308                 renewalperiod   => 7,
3309                 norenewalbefore => undef,
3310                 auto_renew      => 0,
3311                 fine            => .10,
3312                 chargeperiod    => 1,
3313             }
3314         }
3315     );
3316
3317     my $now   = dt_from_string;
3318     my $five_days_go = output_pref({ dt => $now->clone->add( days => 5 ), dateonly => 1});
3319     my $ten_days_go  = output_pref({ dt => $now->clone->add( days => 10), dateonly => 1 });
3320     my $library = $builder->build( { source => 'Branch' } );
3321     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
3322
3323     my $item = $builder->build_sample_item(
3324         {
3325             library      => $library->{branchcode},
3326         }
3327     )->unblessed;
3328
3329     my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $five_days_go ); # date due was 10d ago
3330     my $actualissue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
3331     is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
3332     my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->{barcode},$ten_days_go, undef, undef, undef);
3333     is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
3334     is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
3335 };
3336
3337 subtest 'ItemsDeniedRenewal preference' => sub {
3338     plan tests => 18;
3339
3340     C4::Context->set_preference('ItemsDeniedRenewal','');
3341
3342     my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
3343     Koha::CirculationRules->set_rules(
3344         {
3345             categorycode => '*',
3346             itemtype     => '*',
3347             branchcode   => $idr_lib->branchcode,
3348             rules        => {
3349                 reservesallowed => 25,
3350                 issuelength     => 14,
3351                 lengthunit      => 'days',
3352                 renewalsallowed => 10,
3353                 renewalperiod   => 7,
3354                 norenewalbefore => undef,
3355                 auto_renew      => 0,
3356                 fine            => .10,
3357                 chargeperiod    => 1,
3358             }
3359         }
3360     );
3361
3362     my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
3363         homebranch => $idr_lib->branchcode,
3364         withdrawn => 1,
3365         itype => 'HIDE',
3366         location => 'PROC',
3367         itemcallnumber => undef,
3368         itemnotes => "",
3369         }
3370     });
3371     my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
3372         homebranch => $idr_lib->branchcode,
3373         withdrawn => 0,
3374         itype => 'NOHIDE',
3375         location => 'NOPROC'
3376         }
3377     });
3378
3379     my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
3380         branchcode => $idr_lib->branchcode,
3381         }
3382     });
3383     my $future = dt_from_string->add( days => 1 );
3384     my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3385         returndate => undef,
3386         renewals => 0,
3387         auto_renew => 0,
3388         borrowernumber => $idr_borrower->borrowernumber,
3389         itemnumber => $deny_book->itemnumber,
3390         onsite_checkout => 0,
3391         date_due => $future,
3392         }
3393     });
3394     my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
3395         returndate => undef,
3396         renewals => 0,
3397         auto_renew => 0,
3398         borrowernumber => $idr_borrower->borrowernumber,
3399         itemnumber => $allow_book->itemnumber,
3400         onsite_checkout => 0,
3401         date_due => $future,
3402         }
3403     });
3404
3405     my $idr_rules;
3406
3407     my ( $idr_mayrenew, $idr_error ) =
3408     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3409     is( $idr_mayrenew, 1, 'Renewal allowed when no rules' );
3410     is( $idr_error, undef, 'Renewal allowed when no rules' );
3411
3412     $idr_rules="withdrawn: [1]";
3413
3414     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3415     ( $idr_mayrenew, $idr_error ) =
3416     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3417     is( $idr_mayrenew, 0, 'Renewal blocked when 1 rules (withdrawn)' );
3418     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 1 rule (withdrawn)' );
3419     ( $idr_mayrenew, $idr_error ) =
3420     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3421     is( $idr_mayrenew, 1, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3422     is( $idr_error, undef, 'Renewal allowed when 1 rules not matched (withdrawn)' );
3423
3424     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
3425
3426     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3427     ( $idr_mayrenew, $idr_error ) =
3428     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3429     is( $idr_mayrenew, 0, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
3430     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 2 rules matched (withdrawn,itype)' );
3431     ( $idr_mayrenew, $idr_error ) =
3432     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3433     is( $idr_mayrenew, 1, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3434     is( $idr_error, undef, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
3435
3436     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
3437
3438     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3439     ( $idr_mayrenew, $idr_error ) =
3440     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3441     is( $idr_mayrenew, 0, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
3442     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 3 rules matched (withdrawn,itype, location)' );
3443     ( $idr_mayrenew, $idr_error ) =
3444     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
3445     is( $idr_mayrenew, 1, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3446     is( $idr_error, undef, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
3447
3448     $idr_rules="itemcallnumber: [NULL]";
3449     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3450     ( $idr_mayrenew, $idr_error ) =
3451     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3452     is( $idr_mayrenew, 0, 'Renewal blocked for undef when NULL in pref' );
3453     $idr_rules="itemcallnumber: ['']";
3454     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3455     ( $idr_mayrenew, $idr_error ) =
3456     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3457     is( $idr_mayrenew, 1, 'Renewal not blocked for undef when "" in pref' );
3458
3459     $idr_rules="itemnotes: [NULL]";
3460     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3461     ( $idr_mayrenew, $idr_error ) =
3462     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3463     is( $idr_mayrenew, 1, 'Renewal not blocked for "" when NULL in pref' );
3464     $idr_rules="itemnotes: ['']";
3465     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
3466     ( $idr_mayrenew, $idr_error ) =
3467     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
3468     is( $idr_mayrenew, 0, 'Renewal blocked for empty string when "" in pref' );
3469 };
3470
3471 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
3472     plan tests => 2;
3473
3474     t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3475     my $library = $builder->build( { source => 'Branch' } );
3476     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3477
3478     my $item = $builder->build_sample_item(
3479         {
3480             library      => $library->{branchcode},
3481         }
3482     );
3483
3484     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3485     is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3486     is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3487 };
3488
3489 subtest 'CanBookBeIssued | notforloan' => sub {
3490     plan tests => 2;
3491
3492     t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
3493
3494     my $library = $builder->build( { source => 'Branch' } );
3495     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
3496
3497     my $itemtype = $builder->build(
3498         {
3499             source => 'Itemtype',
3500             value  => { notforloan => undef, }
3501         }
3502     );
3503     my $item = $builder->build_sample_item(
3504         {
3505             library  => $library->{branchcode},
3506             itype    => $itemtype->{itemtype},
3507         }
3508     );
3509     $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3510
3511     my ( $issuingimpossible, $needsconfirmation );
3512
3513
3514     subtest 'item-level_itypes = 1' => sub {
3515         plan tests => 6;
3516
3517         t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
3518         # Is for loan at item type and item level
3519         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3520         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3521         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3522
3523         # not for loan at item type level
3524         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3525         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3526         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3527         is_deeply(
3528             $issuingimpossible,
3529             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3530             'Item can not be issued, not for loan at item type level'
3531         );
3532
3533         # not for loan at item level
3534         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3535         $item->notforloan( 1 )->store;
3536         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3537         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3538         is_deeply(
3539             $issuingimpossible,
3540             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3541             'Item can not be issued, not for loan at item type level'
3542         );
3543     };
3544
3545     subtest 'item-level_itypes = 0' => sub {
3546         plan tests => 6;
3547
3548         t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
3549
3550         # We set another itemtype for biblioitem
3551         my $itemtype = $builder->build(
3552             {
3553                 source => 'Itemtype',
3554                 value  => { notforloan => undef, }
3555             }
3556         );
3557
3558         # for loan at item type and item level
3559         $item->notforloan(0)->store;
3560         $item->biblioitem->itemtype($itemtype->{itemtype})->store;
3561         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3562         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
3563         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
3564
3565         # not for loan at item type level
3566         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
3567         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3568         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3569         is_deeply(
3570             $issuingimpossible,
3571             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
3572             'Item can not be issued, not for loan at item type level'
3573         );
3574
3575         # not for loan at item level
3576         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
3577         $item->notforloan( 1 )->store;
3578         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
3579         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
3580         is_deeply(
3581             $issuingimpossible,
3582             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
3583             'Item can not be issued, not for loan at item type level'
3584         );
3585     };
3586
3587     # TODO test with AllowNotForLoanOverride = 1
3588 };
3589
3590 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
3591     plan tests => 1;
3592
3593     t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
3594     my $item = $builder->build_sample_item(
3595         {
3596             onloan => '2018-01-01',
3597         }
3598     );
3599
3600     AddReturn( $item->barcode, $item->homebranch );
3601     $item->discard_changes; # refresh
3602     is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
3603 };
3604
3605
3606 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
3607
3608     plan tests => 12;
3609
3610
3611     t::lib::Mocks::mock_preference('item-level_itypes', 1);
3612
3613     my $issuing_charges = 15;
3614     my $title   = 'A title';
3615     my $author  = 'Author, An';
3616     my $barcode = 'WHATARETHEODDS';
3617
3618     my $circ = Test::MockModule->new('C4::Circulation');
3619     $circ->mock(
3620         'GetIssuingCharges',
3621         sub {
3622             return $issuing_charges;
3623         }
3624     );
3625
3626     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
3627     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
3628     my $patron   = $builder->build_object({
3629         class => 'Koha::Patrons',
3630         value => { branchcode => $library->id }
3631     });
3632
3633     my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
3634     my $item_id = Koha::Item->new(
3635         {
3636             biblionumber     => $biblio->biblionumber,
3637             homebranch       => $library->id,
3638             holdingbranch    => $library->id,
3639             barcode          => $barcode,
3640             replacementprice => 23.00,
3641             itype            => $itemtype->id
3642         },
3643     )->store->itemnumber;
3644     my $item = Koha::Items->find( $item_id );
3645
3646     my $context = Test::MockModule->new('C4::Context');
3647     $context->mock( userenv => { branch => $library->id } );
3648
3649     # Check the item out
3650     AddIssue( $patron->unblessed, $item->barcode );
3651     t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
3652     my $date = output_pref( { dt => dt_from_string(), dateonly => 1, dateformat => 'iso' } );
3653     my %params_renewal = (
3654         timestamp => { -like => $date . "%" },
3655         module => "CIRCULATION",
3656         action => "RENEWAL",
3657     );
3658     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
3659     AddRenewal( $patron->id, $item->id, $library->id );
3660     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3661     is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
3662
3663     my $checkouts = $patron->checkouts;
3664     # The following will fail if run on 00:00:00
3665     unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
3666
3667     my $lines = Koha::Account::Lines->search({
3668         borrowernumber => $patron->id,
3669         itemnumber     => $item->id
3670     });
3671
3672     is( $lines->count, 2 );
3673
3674     my $line = $lines->next;
3675     is( $line->debit_type_code, 'RENT',       'The issue of item with issuing charge generates an accountline of the correct type' );
3676     is( $line->branchcode,  $library->id, 'AddIssuingCharge correctly sets branchcode' );
3677     is( $line->description, '',     'AddIssue does not set a hardcoded description for the accountline' );
3678
3679     $line = $lines->next;
3680     is( $line->debit_type_code, 'RENT_RENEW', 'The renewal of item with issuing charge generates an accountline of the correct type' );
3681     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
3682     is( $line->description, '', 'AddRenewal does not set a hardcoded description for the accountline' );
3683
3684     t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
3685
3686     $context = Test::MockModule->new('C4::Context');
3687     $context->mock( userenv => { branch => undef, interface => 'CRON'} ); #Test statistical logging of renewal via cron (atuo_renew)
3688
3689     my $now = dt_from_string;
3690     $date = output_pref( { dt => $now, dateonly => 1, dateformat => 'iso' } );
3691     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
3692     my $sth = $dbh->prepare("SELECT COUNT(*) FROM statistics WHERE itemnumber = ? AND branch = ?");
3693     $sth->execute($item->id, $library->id);
3694     my ($old_stats_size) = $sth->fetchrow_array;
3695     AddRenewal( $patron->id, $item->id, $library->id );
3696     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3697     $sth->execute($item->id, $library->id);
3698     my ($new_stats_size) = $sth->fetchrow_array;
3699     is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
3700     is( $new_stats_size, $old_stats_size + 1, 'renew statistic successfully added with passed branch' );
3701
3702     AddReturn( $item->id, $library->id, undef, $date );
3703     AddIssue( $patron->unblessed, $item->barcode, $now );
3704     AddRenewal( $patron->id, $item->id, $library->id, undef, undef, 1 );
3705     my $lines_skipped = Koha::Account::Lines->search({
3706         borrowernumber => $patron->id,
3707         itemnumber     => $item->id
3708     });
3709     is( $lines_skipped->count, 5, 'Passing skipfinecalc causes fine calculation on renewal to be skipped' );
3710
3711 };
3712
3713 subtest 'ProcessOfflinePayment() tests' => sub {
3714
3715     plan tests => 4;
3716
3717
3718     my $amount = 123;
3719
3720     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
3721     my $library = $builder->build_object({ class => 'Koha::Libraries' });
3722     my $result  = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
3723
3724     is( $result, 'Success.', 'The right string is returned' );
3725
3726     my $lines = $patron->account->lines;
3727     is( $lines->count, 1, 'line created correctly');
3728
3729     my $line = $lines->next;
3730     is( $line->amount+0, $amount * -1, 'amount picked from params' );
3731     is( $line->branchcode, $library->id, 'branchcode set correctly' );
3732
3733 };
3734
3735 subtest 'Incremented fee tests' => sub {
3736     plan tests => 19;
3737
3738     my $dt = dt_from_string();
3739     Time::Fake->offset( $dt->epoch );
3740
3741     t::lib::Mocks::mock_preference( 'item-level_itypes', 1 );
3742
3743     my $library =
3744       $builder->build_object( { class => 'Koha::Libraries' } )->store;
3745
3746     my $module = new Test::MockModule('C4::Context');
3747     $module->mock( 'userenv', sub { { branch => $library->id } } );
3748
3749     my $patron = $builder->build_object(
3750         {
3751             class => 'Koha::Patrons',
3752             value => { categorycode => $patron_category->{categorycode} }
3753         }
3754     )->store;
3755
3756     my $itemtype = $builder->build_object(
3757         {
3758             class => 'Koha::ItemTypes',
3759             value => {
3760                 notforloan                   => undef,
3761                 rentalcharge                 => 0,
3762                 rentalcharge_daily           => 1,
3763                 rentalcharge_daily_calendar  => 0
3764             }
3765         }
3766     )->store;
3767
3768     my $item = $builder->build_sample_item(
3769         {
3770             library  => $library->{branchcode},
3771             itype    => $itemtype->id,
3772         }
3773     );
3774
3775     is( $itemtype->rentalcharge_daily+0,
3776         1, 'Daily rental charge stored and retreived correctly' );
3777     is( $item->effective_itemtype, $itemtype->id,
3778         "Itemtype set correctly for item" );
3779
3780     my $now         = dt_from_string;
3781     my $dt_from     = $now->clone;
3782     my $dt_to       = $now->clone->add( days => 7 );
3783     my $dt_to_renew = $now->clone->add( days => 13 );
3784
3785     # Daily Tests
3786     my $issue =
3787       AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3788     my $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3789     is( $accountline->amount+0, 7,
3790 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0"
3791     );
3792     $accountline->delete();
3793     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3794     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3795     is( $accountline->amount+0, 6,
3796 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 0, for renewal"
3797     );
3798     $accountline->delete();
3799     $issue->delete();
3800
3801     t::lib::Mocks::mock_preference( 'finesCalendar', 'noFinesWhenClosed' );
3802     $itemtype->rentalcharge_daily_calendar(1)->store();
3803     $issue =
3804       AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3805     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3806     is( $accountline->amount+0, 7,
3807 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1"
3808     );
3809     $accountline->delete();
3810     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3811     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3812     is( $accountline->amount+0, 6,
3813 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1, for renewal"
3814     );
3815     $accountline->delete();
3816     $issue->delete();
3817
3818     my $calendar = C4::Calendar->new( branchcode => $library->id );
3819     # DateTime 1..7 (Mon..Sun), C4::Calender 0..6 (Sun..Sat)
3820     my $closed_day =
3821         ( $dt_from->day_of_week == 6 ) ? 0
3822       : ( $dt_from->day_of_week == 7 ) ? 1
3823       :                                  $dt_from->day_of_week + 1;
3824     my $closed_day_name = $dt_from->clone->add(days => 1)->day_name;
3825     $calendar->insert_week_day_holiday(
3826         weekday     => $closed_day,
3827         title       => 'Test holiday',
3828         description => 'Test holiday'
3829     );
3830     $issue =
3831       AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3832     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3833     is( $accountline->amount+0, 6,
3834 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name"
3835     );
3836     $accountline->delete();
3837     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3838     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3839     is( $accountline->amount+0, 5,
3840 "Daily rental charge calculated correctly with rentalcharge_daily_calendar = 1 and closed $closed_day_name, for renewal"
3841     );
3842     $accountline->delete();
3843     $issue->delete();
3844
3845     $itemtype->rentalcharge(2)->store;
3846     is( $itemtype->rentalcharge+0, 2,
3847         'Rental charge updated and retreived correctly' );
3848     $issue =
3849       AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3850     my $accountlines =
3851       Koha::Account::Lines->search( { itemnumber => $item->id } );
3852     is( $accountlines->count, '2',
3853         "Fixed charge and accrued charge recorded distinctly" );
3854     $accountlines->delete();
3855     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3856     $accountlines = Koha::Account::Lines->search( { itemnumber => $item->id } );
3857     is( $accountlines->count, '2',
3858         "Fixed charge and accrued charge recorded distinctly, for renewal" );
3859     $accountlines->delete();
3860     $issue->delete();
3861     $itemtype->rentalcharge(0)->store;
3862     is( $itemtype->rentalcharge+0, 0,
3863         'Rental charge reset and retreived correctly' );
3864
3865     # Hourly
3866     Koha::CirculationRules->set_rule(
3867         {
3868             categorycode => $patron->categorycode,
3869             itemtype     => $itemtype->id,
3870             branchcode   => $library->id,
3871             rule_name    => 'lengthunit',
3872             rule_value   => 'hours',
3873         }
3874     );
3875
3876     $itemtype->rentalcharge_hourly('0.25')->store();
3877     is( $itemtype->rentalcharge_hourly,
3878         '0.25', 'Hourly rental charge stored and retreived correctly' );
3879
3880     $dt_to       = $now->clone->add( hours => 168 );
3881     $dt_to_renew = $now->clone->add( hours => 312 );
3882
3883     $itemtype->rentalcharge_hourly_calendar(0)->store();
3884     $issue =
3885       AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3886     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3887     is( $accountline->amount + 0, 42,
3888         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0 (168h * 0.25u)" );
3889     $accountline->delete();
3890     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3891     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3892     is( $accountline->amount + 0, 36,
3893         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 0, for renewal (312h - 168h * 0.25u)" );
3894     $accountline->delete();
3895     $issue->delete();
3896
3897     $itemtype->rentalcharge_hourly_calendar(1)->store();
3898     $issue =
3899       AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3900     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3901     is( $accountline->amount + 0, 36,
3902         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name (168h - 24h * 0.25u)" );
3903     $accountline->delete();
3904     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3905     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3906     is( $accountline->amount + 0, 30,
3907         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 and closed $closed_day_name, for renewal (312h - 168h - 24h * 0.25u" );
3908     $accountline->delete();
3909     $issue->delete();
3910
3911     $calendar->delete_holiday( weekday => $closed_day );
3912     $issue =
3913       AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3914     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3915     is( $accountline->amount + 0, 42,
3916         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1 (168h - 0h * 0.25u" );
3917     $accountline->delete();
3918     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3919     $accountline = Koha::Account::Lines->find( { itemnumber => $item->id } );
3920     is( $accountline->amount + 0, 36,
3921         "Hourly rental charge calculated correctly with rentalcharge_hourly_calendar = 1, for renewal (312h - 168h - 0h * 0.25u)" );
3922     $accountline->delete();
3923     $issue->delete();
3924     Time::Fake->reset;
3925 };
3926
3927 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
3928     plan tests => 2;
3929
3930     t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
3931     t::lib::Mocks::mock_preference('item-level_itypes', 1);
3932
3933     my $library =
3934       $builder->build_object( { class => 'Koha::Libraries' } )->store;
3935     my $patron = $builder->build_object(
3936         {
3937             class => 'Koha::Patrons',
3938             value => { categorycode => $patron_category->{categorycode} }
3939         }
3940     )->store;
3941
3942     my $itemtype = $builder->build_object(
3943         {
3944             class => 'Koha::ItemTypes',
3945             value => {
3946                 notforloan             => 0,
3947                 rentalcharge           => 0,
3948                 rentalcharge_daily => 0
3949             }
3950         }
3951     );
3952
3953     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
3954     my $item = $builder->build_object(
3955         {
3956             class => 'Koha::Items',
3957             value  => {
3958                 homebranch    => $library->id,
3959                 holdingbranch => $library->id,
3960                 notforloan    => 0,
3961                 itemlost      => 0,
3962                 withdrawn     => 0,
3963                 itype         => $itemtype->id,
3964                 biblionumber  => $biblioitem->{biblionumber},
3965                 biblioitemnumber => $biblioitem->{biblioitemnumber},
3966             }
3967         }
3968     )->store;
3969
3970     my ( $issuingimpossible, $needsconfirmation );
3971     my $dt_from = dt_from_string();
3972     my $dt_due = $dt_from->clone->add( days => 3 );
3973
3974     $itemtype->rentalcharge(1)->store;
3975     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
3976     is_deeply( $needsconfirmation, { RENTALCHARGE => '1.00' }, 'Item needs rentalcharge confirmation to be issued' );
3977     $itemtype->rentalcharge('0')->store;
3978     $itemtype->rentalcharge_daily(1)->store;
3979     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
3980     is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
3981     $itemtype->rentalcharge_daily('0')->store;
3982 };
3983
3984 subtest 'Do not return on renewal (LOST charge)' => sub {
3985     plan tests => 1;
3986
3987     t::lib::Mocks::mock_preference('MarkLostItemsAsReturned', 'onpayment');
3988     my $library = $builder->build_object( { class => "Koha::Libraries" } );
3989     my $manager = $builder->build_object( { class => "Koha::Patrons" } );
3990     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
3991
3992     my $biblio = $builder->build_sample_biblio;
3993
3994     my $item = $builder->build_sample_item(
3995         {
3996             biblionumber     => $biblio->biblionumber,
3997             library          => $library->branchcode,
3998             replacementprice => 99.00,
3999             itype            => $itemtype,
4000         }
4001     );
4002
4003     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
4004     AddIssue( $patron->unblessed, $item->barcode );
4005
4006     my $accountline = Koha::Account::Line->new(
4007         {
4008             borrowernumber    => $patron->borrowernumber,
4009             debit_type_code   => 'LOST',
4010             status            => undef,
4011             itemnumber        => $item->itemnumber,
4012             amount            => 12,
4013             amountoutstanding => 12,
4014             interface         => 'something',
4015         }
4016     )->store();
4017
4018     # AddRenewal doesn't call _FixAccountForLostAndFound
4019     AddIssue( $patron->unblessed, $item->barcode );
4020
4021     is( $patron->checkouts->count, 1,
4022         'Renewal should not return the item even if a LOST payment has been made earlier'
4023     );
4024 };
4025
4026 subtest 'Filling a hold should cancel existing transfer' => sub {
4027     plan tests => 4;
4028
4029     t::lib::Mocks::mock_preference('AutomaticItemReturn', 1);
4030
4031     my $libraryA = $builder->build_object( { class => 'Koha::Libraries' } );
4032     my $libraryB = $builder->build_object( { class => 'Koha::Libraries' } );
4033     my $patron = $builder->build_object(
4034         {
4035             class => 'Koha::Patrons',
4036             value => {
4037                 categorycode => $patron_category->{categorycode},
4038                 branchcode => $libraryA->branchcode,
4039             }
4040         }
4041     )->store;
4042
4043     my $item = $builder->build_sample_item({
4044         homebranch => $libraryB->branchcode,
4045     });
4046
4047     my ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4048     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 1, "We generate a transfer on checkin");
4049     AddReserve({
4050         branchcode     => $libraryA->branchcode,
4051         borrowernumber => $patron->borrowernumber,
4052         biblionumber   => $item->biblionumber,
4053         itemnumber     => $item->itemnumber
4054     });
4055     my $reserves = Koha::Holds->search({ itemnumber => $item->itemnumber });
4056     is( $reserves->count, 1, "Reserve is placed");
4057     ( undef, $message ) = AddReturn( $item->barcode, $libraryA->branchcode, undef, undef );
4058     my $reserve = $reserves->next;
4059     ModReserveAffect( $item->itemnumber, $patron->borrowernumber, 0, $reserve->reserve_id );
4060     $reserve->discard_changes;
4061     ok( $reserve->found eq 'W', "Reserve is marked waiting" );
4062     is( Koha::Item::Transfers->search({ itemnumber => $item->itemnumber, datearrived => undef })->count, 0, "No outstanding transfers when hold is waiting");
4063 };
4064
4065 $schema->storage->txn_rollback;
4066 C4::Context->clear_syspref_cache();
4067 $branches = Koha::Libraries->search();
4068 for my $branch ( $branches->next ) {
4069     my $key = $branch->branchcode . "_holidays";
4070     $cache->clear_from_cache($key);
4071 }