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