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