Bug 18925: Move maxissueqty and maxonsiteissueqty to circulation_rules
[koha.git] / t / db_dependent / Circulation.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19 use utf8;
20
21 use Test::More tests => 124;
22 use Test::MockModule;
23
24 use Data::Dumper;
25 use DateTime;
26 use POSIX qw( floor );
27 use t::lib::Mocks;
28 use t::lib::TestBuilder;
29
30 use C4::Accounts;
31 use C4::Calendar;
32 use C4::Circulation;
33 use C4::Biblio;
34 use C4::Items;
35 use C4::Log;
36 use C4::Reserves;
37 use C4::Overdues qw(UpdateFine CalcFine);
38 use Koha::DateUtils;
39 use Koha::Database;
40 use Koha::IssuingRules;
41 use Koha::Items;
42 use Koha::Checkouts;
43 use Koha::Patrons;
44 use Koha::CirculationRules;
45 use Koha::Subscriptions;
46 use Koha::Account::Lines;
47 use Koha::Account::Offsets;
48
49 my $schema = Koha::Database->schema;
50 $schema->storage->txn_begin;
51 my $builder = t::lib::TestBuilder->new;
52 my $dbh = C4::Context->dbh;
53
54 # Start transaction
55 $dbh->{RaiseError} = 1;
56
57 my $cache = Koha::Caches->get_instance();
58 $dbh->do(q|DELETE FROM special_holidays|);
59 $dbh->do(q|DELETE FROM repeatable_holidays|);
60 $cache->clear_from_cache('single_holidays');
61
62 # Start with a clean slate
63 $dbh->do('DELETE FROM issues');
64 $dbh->do('DELETE FROM borrowers');
65
66 my $library = $builder->build({
67     source => 'Branch',
68 });
69 my $library2 = $builder->build({
70     source => 'Branch',
71 });
72 my $itemtype = $builder->build(
73     {   source => 'Itemtype',
74         value  => { notforloan => undef, rentalcharge => 0, defaultreplacecost => undef, processfee => undef }
75     }
76 )->{itemtype};
77 my $patron_category = $builder->build(
78     {
79         source => 'Category',
80         value  => {
81             category_type                 => 'P',
82             enrolmentfee                  => 0,
83             BlockExpiredPatronOpacActions => -1, # Pick the pref value
84         }
85     }
86 );
87
88 my $CircControl = C4::Context->preference('CircControl');
89 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
90
91 my $item = {
92     homebranch => $library2->{branchcode},
93     holdingbranch => $library2->{branchcode}
94 };
95
96 my $borrower = {
97     branchcode => $library2->{branchcode}
98 };
99
100 # No userenv, PickupLibrary
101 t::lib::Mocks::mock_preference('IndependentBranches', '0');
102 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
103 is(
104     C4::Context->preference('CircControl'),
105     'PickupLibrary',
106     'CircControl changed to PickupLibrary'
107 );
108 is(
109     C4::Circulation::_GetCircControlBranch($item, $borrower),
110     $item->{$HomeOrHoldingBranch},
111     '_GetCircControlBranch returned item branch (no userenv defined)'
112 );
113
114 # No userenv, PatronLibrary
115 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
116 is(
117     C4::Context->preference('CircControl'),
118     'PatronLibrary',
119     'CircControl changed to PatronLibrary'
120 );
121 is(
122     C4::Circulation::_GetCircControlBranch($item, $borrower),
123     $borrower->{branchcode},
124     '_GetCircControlBranch returned borrower branch'
125 );
126
127 # No userenv, ItemHomeLibrary
128 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
129 is(
130     C4::Context->preference('CircControl'),
131     'ItemHomeLibrary',
132     'CircControl changed to ItemHomeLibrary'
133 );
134 is(
135     $item->{$HomeOrHoldingBranch},
136     C4::Circulation::_GetCircControlBranch($item, $borrower),
137     '_GetCircControlBranch returned item branch'
138 );
139
140 # Now, set a userenv
141 t::lib::Mocks::mock_userenv({ branchcode => $library2->{branchcode} });
142 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
143
144 # Userenv set, PickupLibrary
145 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
146 is(
147     C4::Context->preference('CircControl'),
148     'PickupLibrary',
149     'CircControl changed to PickupLibrary'
150 );
151 is(
152     C4::Circulation::_GetCircControlBranch($item, $borrower),
153     $library2->{branchcode},
154     '_GetCircControlBranch returned current branch'
155 );
156
157 # Userenv set, PatronLibrary
158 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
159 is(
160     C4::Context->preference('CircControl'),
161     'PatronLibrary',
162     'CircControl changed to PatronLibrary'
163 );
164 is(
165     C4::Circulation::_GetCircControlBranch($item, $borrower),
166     $borrower->{branchcode},
167     '_GetCircControlBranch returned borrower branch'
168 );
169
170 # Userenv set, ItemHomeLibrary
171 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
172 is(
173     C4::Context->preference('CircControl'),
174     'ItemHomeLibrary',
175     'CircControl changed to ItemHomeLibrary'
176 );
177 is(
178     C4::Circulation::_GetCircControlBranch($item, $borrower),
179     $item->{$HomeOrHoldingBranch},
180     '_GetCircControlBranch returned item branch'
181 );
182
183 # Reset initial configuration
184 t::lib::Mocks::mock_preference('CircControl', $CircControl);
185 is(
186     C4::Context->preference('CircControl'),
187     $CircControl,
188     'CircControl reset to its initial value'
189 );
190
191 # Set a simple circ policy
192 $dbh->do('DELETE FROM issuingrules');
193 Koha::CirculationRules->search()->delete();
194 $dbh->do(
195     q{INSERT INTO issuingrules (categorycode, branchcode, itemtype, reservesallowed,
196                                 issuelength, lengthunit,
197                                 renewalsallowed, renewalperiod,
198                                 norenewalbefore, auto_renew,
199                                 fine, chargeperiod)
200       VALUES (?, ?, ?, ?,
201               ?, ?,
202               ?, ?,
203               ?, ?,
204               ?, ?
205              )
206     },
207     {},
208     '*', '*', '*', 25,
209     14, 'days',
210     1, 7,
211     undef, 0,
212     .10, 1
213 );
214
215 my ( $reused_itemnumber_1, $reused_itemnumber_2 );
216 {
217 # CanBookBeRenewed tests
218     C4::Context->set_preference('ItemsDeniedRenewal','');
219     # Generate test biblio
220     my $biblio = $builder->build_sample_biblio();
221
222     my $branch = $library2->{branchcode};
223
224     my $item_1 = $builder->build_sample_item(
225         {
226             biblionumber     => $biblio->biblionumber,
227             library          => $branch,
228             replacementprice => 12.00,
229             itype            => $itemtype
230         }
231     );
232     $reused_itemnumber_1 = $item_1->itemnumber;
233
234     my $item_2 = $builder->build_sample_item(
235         {
236             biblionumber     => $biblio->biblionumber,
237             library          => $branch,
238             replacementprice => 23.00,
239             itype            => $itemtype
240         }
241     );
242     $reused_itemnumber_2 = $item_2->itemnumber;
243
244     my $item_3 = $builder->build_sample_item(
245         {
246             biblionumber     => $biblio->biblionumber,
247             library          => $branch,
248             replacementprice => 23.00,
249             itype            => $itemtype
250         }
251     );
252
253     # Create borrowers
254     my %renewing_borrower_data = (
255         firstname =>  'John',
256         surname => 'Renewal',
257         categorycode => $patron_category->{categorycode},
258         branchcode => $branch,
259     );
260
261     my %reserving_borrower_data = (
262         firstname =>  'Katrin',
263         surname => 'Reservation',
264         categorycode => $patron_category->{categorycode},
265         branchcode => $branch,
266     );
267
268     my %hold_waiting_borrower_data = (
269         firstname =>  'Kyle',
270         surname => 'Reservation',
271         categorycode => $patron_category->{categorycode},
272         branchcode => $branch,
273     );
274
275     my %restricted_borrower_data = (
276         firstname =>  'Alice',
277         surname => 'Reservation',
278         categorycode => $patron_category->{categorycode},
279         debarred => '3228-01-01',
280         branchcode => $branch,
281     );
282
283     my %expired_borrower_data = (
284         firstname =>  'Ça',
285         surname => 'Glisse',
286         categorycode => $patron_category->{categorycode},
287         branchcode => $branch,
288         dateexpiry => dt_from_string->subtract( months => 1 ),
289     );
290
291     my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
292     my $reserving_borrowernumber = Koha::Patron->new(\%reserving_borrower_data)->store->borrowernumber;
293     my $hold_waiting_borrowernumber = Koha::Patron->new(\%hold_waiting_borrower_data)->store->borrowernumber;
294     my $restricted_borrowernumber = Koha::Patron->new(\%restricted_borrower_data)->store->borrowernumber;
295     my $expired_borrowernumber = Koha::Patron->new(\%expired_borrower_data)->store->borrowernumber;
296
297     my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
298     my $restricted_borrower = Koha::Patrons->find( $restricted_borrowernumber )->unblessed;
299     my $expired_borrower = Koha::Patrons->find( $expired_borrowernumber )->unblessed;
300
301     my $bibitems       = '';
302     my $priority       = '1';
303     my $resdate        = undef;
304     my $expdate        = undef;
305     my $notes          = '';
306     my $checkitem      = undef;
307     my $found          = undef;
308
309     my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
310     my $datedue = dt_from_string( $issue->date_due() );
311     is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
312
313     my $issue2 = AddIssue( $renewing_borrower, $item_2->barcode);
314     $datedue = dt_from_string( $issue->date_due() );
315     is (defined $issue2, 1, "Item 2 checked out, due date: " . $issue2->date_due());
316
317
318     my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber;
319     is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}");
320
321     my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
322     is( $renewokay, 1, 'Can renew, no holds for this title or item');
323
324
325     # Biblio-level hold, renewal test
326     AddReserve(
327         $branch, $reserving_borrowernumber, $biblio->biblionumber,
328         $bibitems,  $priority, $resdate, $expdate, $notes,
329         'a title', $checkitem, $found
330     );
331
332     # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
333     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
334     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
335     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
336     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
337     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
338     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
339
340     # Now let's add an item level hold, we should no longer be able to renew the item
341     my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
342         {
343             borrowernumber => $hold_waiting_borrowernumber,
344             biblionumber   => $biblio->biblionumber,
345             itemnumber     => $item_1->itemnumber,
346             branchcode     => $branch,
347             priority       => 3,
348         }
349     );
350     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
351     is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
352     $hold->delete();
353
354     # 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
355     # be able to renew these items
356     $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
357         {
358             borrowernumber => $hold_waiting_borrowernumber,
359             biblionumber   => $biblio->biblionumber,
360             itemnumber     => $item_3->itemnumber,
361             branchcode     => $branch,
362             priority       => 0,
363             found          => 'W'
364         }
365     );
366     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
367     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
368     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
369     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
370     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
371
372     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
373     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
374     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
375
376     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
377     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
378     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
379
380     my $reserveid = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next->reserve_id;
381     my $reserving_borrower = Koha::Patrons->find( $reserving_borrowernumber )->unblessed;
382     AddIssue($reserving_borrower, $item_3->barcode);
383     my $reserve = $dbh->selectrow_hashref(
384         'SELECT * FROM old_reserves WHERE reserve_id = ?',
385         { Slice => {} },
386         $reserveid
387     );
388     is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
389
390     # Item-level hold, renewal test
391     AddReserve(
392         $branch, $reserving_borrowernumber, $biblio->biblionumber,
393         $bibitems,  $priority, $resdate, $expdate, $notes,
394         'a title', $item_1->itemnumber, $found
395     );
396
397     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
398     is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
399     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
400
401     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber, 1);
402     is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
403
404     # Items can't fill hold for reasons
405     ModItem({ notforloan => 1 }, $biblio->biblionumber, $item_1->itemnumber);
406     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
407     is( $renewokay, 1, 'Can renew, item is marked not for loan, hold does not block');
408     ModItem({ notforloan => 0, itype => $itemtype }, $biblio->biblionumber, $item_1->itemnumber);
409
410     # FIXME: Add more for itemtype not for loan etc.
411
412     # Restricted users cannot renew when RestrictionBlockRenewing is enabled
413     my $item_5 = $builder->build_sample_item(
414         {
415             biblionumber     => $biblio->biblionumber,
416             library          => $branch,
417             replacementprice => 23.00,
418             itype            => $itemtype,
419         }
420     );
421     my $datedue5 = AddIssue($restricted_borrower, $item_5->barcode);
422     is (defined $datedue5, 1, "Item with date due checked out, due date: $datedue5");
423
424     t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
425     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_2->itemnumber);
426     is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
427     ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $item_5->itemnumber);
428     is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
429
430     # Users cannot renew an overdue item
431     my $item_6 = $builder->build_sample_item(
432         {
433             biblionumber     => $biblio->biblionumber,
434             library          => $branch,
435             replacementprice => 23.00,
436             itype            => $itemtype,
437         }
438     );
439
440     my $item_7 = $builder->build_sample_item(
441         {
442             biblionumber     => $biblio->biblionumber,
443             library          => $branch,
444             replacementprice => 23.00,
445             itype            => $itemtype,
446         }
447     );
448
449     my $datedue6 = AddIssue( $renewing_borrower, $item_6->barcode);
450     is (defined $datedue6, 1, "Item 2 checked out, due date: ".$datedue6->date_due);
451
452     my $now = dt_from_string();
453     my $five_weeks = DateTime::Duration->new(weeks => 5);
454     my $five_weeks_ago = $now - $five_weeks;
455     t::lib::Mocks::mock_preference('finesMode', 'production');
456
457     my $passeddatedue1 = AddIssue($renewing_borrower, $item_7->barcode, $five_weeks_ago);
458     is (defined $passeddatedue1, 1, "Item with passed date due checked out, due date: " . $passeddatedue1->date_due);
459
460     my ( $fine ) = CalcFine( $item_7->unblessed, $renewing_borrower->{categorycode}, $branch, $five_weeks_ago, $now );
461     C4::Overdues::UpdateFine(
462         {
463             issue_id       => $passeddatedue1->id(),
464             itemnumber     => $item_7->itemnumber,
465             borrowernumber => $renewing_borrower->{borrowernumber},
466             amount         => $fine,
467             due            => Koha::DateUtils::output_pref($five_weeks_ago)
468         }
469     );
470
471     t::lib::Mocks::mock_preference('RenewalLog', 0);
472     my $date = output_pref( { dt => dt_from_string(), datenonly => 1, dateformat => 'iso' } );
473     my $old_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["RENEWAL"]) } );
474     AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
475     my $new_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["RENEWAL"]) } );
476     is ($new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog');
477
478     t::lib::Mocks::mock_preference('RenewalLog', 1);
479     $date = output_pref( { dt => dt_from_string(), datenonly => 1, dateformat => 'iso' } );
480     $old_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["RENEWAL"]) } );
481     AddRenewal( $renewing_borrower->{borrowernumber}, $item_7->itemnumber, $branch );
482     $new_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["RENEWAL"]) } );
483     is ($new_log_size, $old_log_size + 1, 'renew log successfully added');
484
485     my $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
486     is( $fines->count, 2 );
487     is( $fines->next->accounttype, 'F', 'Fine on renewed item is closed out properly' );
488     is( $fines->next->accounttype, 'F', 'Fine on renewed item is closed out properly' );
489     $fines->delete();
490
491
492     my $old_issue_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["ISSUE"]) } );
493     my $old_renew_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["RENEWAL"]) } );
494     AddIssue( $renewing_borrower,$item_7->barcode,Koha::DateUtils::output_pref({str=>$datedue6->date_due, dateformat =>'iso'}),0,$date, 0, undef );
495     $new_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["RENEWAL"]) } );
496     is ($new_log_size, $old_renew_log_size + 1, 'renew log successfully added when renewed via issuing');
497     $new_log_size =  scalar(@{GetLogs( $date, $date, undef,["CIRCULATION"], ["ISSUE"]) } );
498     is ($new_log_size, $old_issue_log_size, 'renew not logged as issue when renewed via issuing');
499
500     $fines = Koha::Account::Lines->search( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $item_7->itemnumber } );
501     $fines->delete();
502
503     t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
504     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
505     is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
506     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
507     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
508
509
510     $hold = Koha::Holds->search({ biblionumber => $biblio->biblionumber, borrowernumber => $reserving_borrowernumber })->next;
511     $hold->cancel;
512
513     # Bug 14101
514     # Test automatic renewal before value for "norenewalbefore" in policy is set
515     # In this case automatic renewal is not permitted prior to due date
516     my $item_4 = $builder->build_sample_item(
517         {
518             biblionumber     => $biblio->biblionumber,
519             library          => $branch,
520             replacementprice => 16.00,
521             itype            => $itemtype,
522         }
523     );
524
525     $issue = AddIssue( $renewing_borrower, $item_4->barcode, undef, undef, undef, undef, { auto_renew => 1 } );
526     ( $renewokay, $error ) =
527       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
528     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
529     is( $error, 'auto_too_soon',
530         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
531
532     # Bug 7413
533     # Test premature manual renewal
534     $dbh->do('UPDATE issuingrules SET norenewalbefore = 7');
535
536     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
537     is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
538     is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
539
540     # Bug 14395
541     # Test 'exact time' setting for syspref NoRenewalBeforePrecision
542     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
543     is(
544         GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
545         $datedue->clone->add( days => -7 ),
546         'Bug 14395: Renewals permitted 7 days before due date, as expected'
547     );
548
549     # Bug 14395
550     # Test 'date' setting for syspref NoRenewalBeforePrecision
551     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
552     is(
553         GetSoonestRenewDate( $renewing_borrowernumber, $item_1->itemnumber ),
554         $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
555         'Bug 14395: Renewals permitted 7 days before due date, as expected'
556     );
557
558     # Bug 14101
559     # Test premature automatic renewal
560     ( $renewokay, $error ) =
561       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
562     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
563     is( $error, 'auto_too_soon',
564         'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
565     );
566
567     # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
568     # and test automatic renewal again
569     $dbh->do('UPDATE issuingrules SET norenewalbefore = 0');
570     ( $renewokay, $error ) =
571       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
572     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
573     is( $error, 'auto_too_soon',
574         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
575     );
576
577     # Change policy so that loans can be renewed 99 days prior to the due date
578     # and test automatic renewal again
579     $dbh->do('UPDATE issuingrules SET norenewalbefore = 99');
580     ( $renewokay, $error ) =
581       CanBookBeRenewed( $renewing_borrowernumber, $item_4->itemnumber );
582     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
583     is( $error, 'auto_renew',
584         'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
585     );
586
587     subtest "too_late_renewal / no_auto_renewal_after" => sub {
588         plan tests => 14;
589         my $item_to_auto_renew = $builder->build(
590             {   source => 'Item',
591                 value  => {
592                     biblionumber  => $biblio->biblionumber,
593                     homebranch    => $branch,
594                     holdingbranch => $branch,
595                 }
596             }
597         );
598
599         my $ten_days_before = dt_from_string->add( days => -10 );
600         my $ten_days_ahead  = dt_from_string->add( days => 10 );
601         AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
602
603         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = 9');
604         ( $renewokay, $error ) =
605           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
606         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
607         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
608
609         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = 10');
610         ( $renewokay, $error ) =
611           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
612         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
613         is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
614
615         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = 11');
616         ( $renewokay, $error ) =
617           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
618         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
619         is( $error, 'auto_too_soon', 'Cannot auto renew, too soon - no_auto_renewal_after is defined(returned code is auto_too_soon)' );
620
621         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 11');
622         ( $renewokay, $error ) =
623           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
624         is( $renewokay, 0,            'Do not renew, renewal is automatic' );
625         is( $error,     'auto_renew', 'Cannot renew, renew is automatic' );
626
627         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = NULL, no_auto_renewal_after_hard_limit = ?', undef, dt_from_string->add( days => -1 ) );
628         ( $renewokay, $error ) =
629           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
630         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
631         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
632
633         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = 15, no_auto_renewal_after_hard_limit = ?', undef, dt_from_string->add( days => -1 ) );
634         ( $renewokay, $error ) =
635           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
636         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
637         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
638
639         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = NULL, no_auto_renewal_after_hard_limit = ?', undef, dt_from_string->add( days => 1 ) );
640         ( $renewokay, $error ) =
641           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
642         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
643         is( $error, 'auto_renew', 'Cannot renew, renew is automatic' );
644     };
645
646     subtest "auto_too_much_oweing | OPACFineNoRenewalsBlockAutoRenew" => sub {
647         plan tests => 6;
648         my $item_to_auto_renew = $builder->build({
649             source => 'Item',
650             value => {
651                 biblionumber => $biblio->biblionumber,
652                 homebranch       => $branch,
653                 holdingbranch    => $branch,
654             }
655         });
656
657         my $ten_days_before = dt_from_string->add( days => -10 );
658         my $ten_days_ahead = dt_from_string->add( days => 10 );
659         AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
660
661         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 11');
662         C4::Context->set_preference('OPACFineNoRenewalsBlockAutoRenew','1');
663         C4::Context->set_preference('OPACFineNoRenewals','10');
664         my $fines_amount = 5;
665         C4::Accounts::manualinvoice( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber}, "Some fines", 'F', $fines_amount );
666         ( $renewokay, $error ) =
667           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
668         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
669         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 5' );
670
671         C4::Accounts::manualinvoice( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber}, "Some fines", 'F', $fines_amount );
672         ( $renewokay, $error ) =
673           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
674         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
675         is( $error, 'auto_renew', 'Can auto renew, OPACFineNoRenewals=10, patron has 10' );
676
677         C4::Accounts::manualinvoice( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber}, "Some fines", 'F', $fines_amount );
678         ( $renewokay, $error ) =
679           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
680         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
681         is( $error, 'auto_too_much_oweing', 'Cannot auto renew, OPACFineNoRenewals=10, patron has 15' );
682
683         $dbh->do('DELETE FROM accountlines WHERE borrowernumber=?', undef, $renewing_borrowernumber);
684     };
685
686     subtest "auto_account_expired | BlockExpiredPatronOpacActions" => sub {
687         plan tests => 6;
688         my $item_to_auto_renew = $builder->build({
689             source => 'Item',
690             value => {
691                 biblionumber => $biblio->biblionumber,
692                 homebranch       => $branch,
693                 holdingbranch    => $branch,
694             }
695         });
696
697         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 11');
698
699         my $ten_days_before = dt_from_string->add( days => -10 );
700         my $ten_days_ahead = dt_from_string->add( days => 10 );
701
702         # Patron is expired and BlockExpiredPatronOpacActions=0
703         # => auto renew is allowed
704         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0);
705         my $patron = $expired_borrower;
706         my $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
707         ( $renewokay, $error ) =
708           CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
709         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
710         is( $error, 'auto_renew', 'Can auto renew, patron is expired but BlockExpiredPatronOpacActions=0' );
711         Koha::Checkouts->find( $checkout->issue_id )->delete;
712
713
714         # Patron is expired and BlockExpiredPatronOpacActions=1
715         # => auto renew is not allowed
716         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
717         $patron = $expired_borrower;
718         $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
719         ( $renewokay, $error ) =
720           CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
721         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
722         is( $error, 'auto_account_expired', 'Can not auto renew, lockExpiredPatronOpacActions=1 and patron is expired' );
723         Koha::Checkouts->find( $checkout->issue_id )->delete;
724
725
726         # Patron is not expired and BlockExpiredPatronOpacActions=1
727         # => auto renew is allowed
728         t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
729         $patron = $renewing_borrower;
730         $checkout = AddIssue( $patron, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
731         ( $renewokay, $error ) =
732           CanBookBeRenewed( $patron->{borrowernumber}, $item_to_auto_renew->{itemnumber} );
733         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
734         is( $error, 'auto_renew', 'Can auto renew, BlockExpiredPatronOpacActions=1 but patron is not expired' );
735         Koha::Checkouts->find( $checkout->issue_id )->delete;
736     };
737
738     subtest "GetLatestAutoRenewDate" => sub {
739         plan tests => 5;
740         my $item_to_auto_renew = $builder->build(
741             {   source => 'Item',
742                 value  => {
743                     biblionumber  => $biblio->biblionumber,
744                     homebranch    => $branch,
745                     holdingbranch => $branch,
746                 }
747             }
748         );
749
750         my $ten_days_before = dt_from_string->add( days => -10 );
751         my $ten_days_ahead  = dt_from_string->add( days => 10 );
752         AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
753         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = NULL, no_auto_renewal_after_hard_limit = NULL');
754         my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
755         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' );
756         my $five_days_before = dt_from_string->add( days => -5 );
757         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 5, no_auto_renewal_after_hard_limit = NULL');
758         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
759         is( $latest_auto_renew_date->truncate( to => 'minute' ),
760             $five_days_before->truncate( to => 'minute' ),
761             'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
762         );
763         my $five_days_ahead = dt_from_string->add( days => 5 );
764         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 15, no_auto_renewal_after_hard_limit = NULL');
765         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
766         is( $latest_auto_renew_date->truncate( to => 'minute' ),
767             $five_days_ahead->truncate( to => 'minute' ),
768             'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
769         );
770         my $two_days_ahead = dt_from_string->add( days => 2 );
771         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = NULL, no_auto_renewal_after_hard_limit = ?', undef, dt_from_string->add( days => 2 ) );
772         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
773         is( $latest_auto_renew_date->truncate( to => 'day' ),
774             $two_days_ahead->truncate( to => 'day' ),
775             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is defined and not no_auto_renewal_after'
776         );
777         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 15, no_auto_renewal_after_hard_limit = ?', undef, dt_from_string->add( days => 2 ) );
778         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
779         is( $latest_auto_renew_date->truncate( to => 'day' ),
780             $two_days_ahead->truncate( to => 'day' ),
781             'GetLatestAutoRenewDate should return +2 days if no_auto_renewal_after_hard_limit is < no_auto_renewal_after'
782         );
783
784     };
785
786     # Too many renewals
787
788     # set policy to forbid renewals
789     $dbh->do('UPDATE issuingrules SET norenewalbefore = NULL, renewalsallowed = 0');
790
791     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber);
792     is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
793     is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
794
795     # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
796     t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
797     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
798
799     C4::Overdues::UpdateFine(
800         {
801             issue_id       => $issue->id(),
802             itemnumber     => $item_1->itemnumber,
803             borrowernumber => $renewing_borrower->{borrowernumber},
804             amount         => 15.00,
805             type           => q{},
806             due            => Koha::DateUtils::output_pref($datedue)
807         }
808     );
809
810     my $line = Koha::Account::Lines->search({ borrowernumber => $renewing_borrower->{borrowernumber} })->next();
811     is( $line->accounttype, 'FU', 'Account line type is FU' );
812     is( $line->lastincrement, '15.000000', 'Account line last increment is 15.00' );
813     is( $line->amountoutstanding, '15.000000', 'Account line amount outstanding is 15.00' );
814     is( $line->amount, '15.000000', 'Account line amount is 15.00' );
815     is( $line->issue_id, $issue->id, 'Account line issue id matches' );
816
817     my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
818     is( $offset->type, 'Fine', 'Account offset type is Fine' );
819     is( $offset->amount, '15.000000', 'Account offset amount is 15.00' );
820
821     t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
822     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
823
824     LostItem( $item_1->itemnumber, 'test', 1 );
825
826     $line = Koha::Account::Lines->find($line->id);
827     is( $line->accounttype, 'F', 'Account type correctly changed from FU to F' );
828
829     my $item = Koha::Items->find($item_1->itemnumber);
830     ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
831     my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
832     is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
833
834     my $total_due = $dbh->selectrow_array(
835         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
836         undef, $renewing_borrower->{borrowernumber}
837     );
838
839     is( $total_due, '15.000000', 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
840
841     C4::Context->dbh->do("DELETE FROM accountlines");
842
843     C4::Overdues::UpdateFine(
844         {
845             issue_id       => $issue2->id(),
846             itemnumber     => $item_2->itemnumber,
847             borrowernumber => $renewing_borrower->{borrowernumber},
848             amount         => 15.00,
849             type           => q{},
850             due            => Koha::DateUtils::output_pref($datedue)
851         }
852     );
853
854     LostItem( $item_2->itemnumber, 'test', 0 );
855
856     my $item2 = Koha::Items->find($item_2->itemnumber);
857     ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
858     ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
859
860     $total_due = $dbh->selectrow_array(
861         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
862         undef, $renewing_borrower->{borrowernumber}
863     );
864
865     ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
866
867     my $future = dt_from_string();
868     $future->add( days => 7 );
869     my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
870     ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
871
872     # Users cannot renew any item if there is an overdue item
873     t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
874     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
875     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
876     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
877     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
878
879     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
880     $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
881     LostItem( $item_3->itemnumber, 'test', 0 );
882     my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
883     is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
884   }
885
886 {
887     # GetUpcomingDueIssues tests
888     my $branch   = $library2->{branchcode};
889
890     #Create another record
891     my $biblio2 = $builder->build_sample_biblio();
892
893     #Create third item
894     my $item_1 = Koha::Items->find($reused_itemnumber_1);
895     my $item_2 = Koha::Items->find($reused_itemnumber_2);
896     my $item_3 = $builder->build_sample_item(
897         {
898             biblionumber     => $biblio2->biblionumber,
899             library          => $branch,
900             itype            => $itemtype,
901         }
902     );
903
904
905     # Create a borrower
906     my %a_borrower_data = (
907         firstname =>  'Fridolyn',
908         surname => 'SOMERS',
909         categorycode => $patron_category->{categorycode},
910         branchcode => $branch,
911     );
912
913     my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
914     my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber )->unblessed;
915
916     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
917     my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
918     my $today = DateTime->today(time_zone => C4::Context->tz());
919
920     my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
921     my $datedue = dt_from_string( $issue->date_due() );
922     my $issue2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
923     my $datedue2 = dt_from_string( $issue->date_due() );
924
925     my $upcoming_dues;
926
927     # GetUpcomingDueIssues tests
928     for my $i(0..1) {
929         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
930         is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
931     }
932
933     #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
934     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
935     is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
936
937     for my $i(3..5) {
938         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
939         is ( scalar( @$upcoming_dues ), 1,
940             "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
941     }
942
943     # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
944
945     my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
946
947     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
948     is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
949
950     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
951     is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
952
953     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
954     is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
955
956     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 }  );
957     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
958
959     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
960     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
961
962     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
963     is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
964
965 }
966
967 {
968     my $branch   = $library2->{branchcode};
969
970     my $biblio = $builder->build_sample_biblio();
971
972     #Create third item
973     my $item = $builder->build_sample_item(
974         {
975             biblionumber     => $biblio->biblionumber,
976             library          => $branch,
977             itype            => $itemtype,
978         }
979     );
980
981     # Create a borrower
982     my %a_borrower_data = (
983         firstname =>  'Kyle',
984         surname => 'Hall',
985         categorycode => $patron_category->{categorycode},
986         branchcode => $branch,
987     );
988
989     my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
990
991     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
992     my $issue = AddIssue( $borrower, $item->barcode );
993     UpdateFine(
994         {
995             issue_id       => $issue->id(),
996             itemnumber     => $item->itemnumber,
997             borrowernumber => $borrowernumber,
998             amount         => 0,
999             type           => q{}
1000         }
1001     );
1002
1003     my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1004     my $count = $hr->{count};
1005
1006     is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1007 }
1008
1009 {
1010     $dbh->do('DELETE FROM issues');
1011     $dbh->do('DELETE FROM items');
1012     $dbh->do('DELETE FROM issuingrules');
1013     Koha::CirculationRules->search()->delete();
1014     $dbh->do(
1015         q{
1016         INSERT INTO issuingrules ( categorycode, branchcode, itemtype, reservesallowed, issuelength, lengthunit, renewalsallowed, renewalperiod,
1017                     norenewalbefore, auto_renew, fine, chargeperiod ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
1018         },
1019         {},
1020         '*', '*', '*', 25,
1021         14,  'days',
1022         1,   7,
1023         undef,  0,
1024         .10, 1
1025     );
1026     Koha::CirculationRules->set_rules(
1027         {
1028             categorycode => '*',
1029             itemtype     => '*',
1030             branchcode   => '*',
1031             rules        => {
1032                 maxissueqty => 20
1033             }
1034         }
1035     );
1036     my $biblio = $builder->build_sample_biblio();
1037
1038     my $item_1 = $builder->build_sample_item(
1039         {
1040             biblionumber     => $biblio->biblionumber,
1041             library          => $library2->{branchcode},
1042             itype            => $itemtype,
1043         }
1044     );
1045
1046     my $item_2= $builder->build_sample_item(
1047         {
1048             biblionumber     => $biblio->biblionumber,
1049             library          => $library2->{branchcode},
1050             itype            => $itemtype,
1051         }
1052     );
1053
1054     my $borrowernumber1 = Koha::Patron->new({
1055         firstname    => 'Kyle',
1056         surname      => 'Hall',
1057         categorycode => $patron_category->{categorycode},
1058         branchcode   => $library2->{branchcode},
1059     })->store->borrowernumber;
1060     my $borrowernumber2 = Koha::Patron->new({
1061         firstname    => 'Chelsea',
1062         surname      => 'Hall',
1063         categorycode => $patron_category->{categorycode},
1064         branchcode   => $library2->{branchcode},
1065     })->store->borrowernumber;
1066
1067     my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
1068     my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
1069
1070     my $issue = AddIssue( $borrower1, $item_1->barcode );
1071
1072     my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1073     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1074
1075     AddReserve(
1076         $library2->{branchcode}, $borrowernumber2, $biblio->biblionumber,
1077         '',  1, undef, undef, '',
1078         undef, undef, undef
1079     );
1080
1081     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 0");
1082     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1083     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1084     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1085
1086     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 0");
1087     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1088     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1089     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1090
1091     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
1092     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1093     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1094     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1095
1096     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
1097     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1098     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1099     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1100
1101     # Setting item not checked out to be not for loan but holdable
1102     ModItem({ notforloan => -1 }, $biblio->biblionumber, $item_2->itemnumber);
1103
1104     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1105     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' );
1106 }
1107
1108 {
1109     # Don't allow renewing onsite checkout
1110     my $branch   = $library->{branchcode};
1111
1112     #Create another record
1113     my $biblio = $builder->build_sample_biblio();
1114
1115     my $item = $builder->build_sample_item(
1116         {
1117             biblionumber     => $biblio->biblionumber,
1118             library          => $branch,
1119             itype            => $itemtype,
1120         }
1121     );
1122
1123     my $borrowernumber = Koha::Patron->new({
1124         firstname =>  'fn',
1125         surname => 'dn',
1126         categorycode => $patron_category->{categorycode},
1127         branchcode => $branch,
1128     })->store->borrowernumber;
1129
1130     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1131
1132     my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
1133     my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $item->itemnumber );
1134     is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
1135     is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
1136 }
1137
1138 {
1139     my $library = $builder->build({ source => 'Branch' });
1140
1141     my $biblio = $builder->build_sample_biblio();
1142
1143     my $item = $builder->build_sample_item(
1144         {
1145             biblionumber     => $biblio->biblionumber,
1146             library          => $library->{branchcode},
1147             itype            => $itemtype,
1148         }
1149     );
1150
1151     my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
1152
1153     my $issue = AddIssue( $patron, $item->barcode );
1154     UpdateFine(
1155         {
1156             issue_id       => $issue->id(),
1157             itemnumber     => $item->itemnumber,
1158             borrowernumber => $patron->{borrowernumber},
1159             amount         => 1,
1160             type           => q{}
1161         }
1162     );
1163     UpdateFine(
1164         {
1165             issue_id       => $issue->id(),
1166             itemnumber     => $item->itemnumber,
1167             borrowernumber => $patron->{borrowernumber},
1168             amount         => 2,
1169             type           => q{}
1170         }
1171     );
1172     is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1173 }
1174
1175 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1176     plan tests => 24;
1177
1178     my $homebranch    = $builder->build( { source => 'Branch' } );
1179     my $holdingbranch = $builder->build( { source => 'Branch' } );
1180     my $otherbranch   = $builder->build( { source => 'Branch' } );
1181     my $patron_1      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1182     my $patron_2      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1183
1184     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1185     my $item = $builder->build(
1186         {   source => 'Item',
1187             value  => {
1188                 homebranch    => $homebranch->{branchcode},
1189                 holdingbranch => $holdingbranch->{branchcode},
1190                 biblionumber  => $biblioitem->{biblionumber}
1191             }
1192         }
1193     );
1194
1195     set_userenv($holdingbranch);
1196
1197     my $issue = AddIssue( $patron_1->unblessed, $item->{barcode} );
1198     is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
1199
1200     my ( $error, $question, $alerts );
1201
1202     # AllowReturnToBranch == anywhere
1203     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1204     ## Test that unknown barcodes don't generate internal server errors
1205     set_userenv($homebranch);
1206     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
1207     ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
1208     ## Can be issued from homebranch
1209     set_userenv($homebranch);
1210     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1211     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1212     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1213     ## Can be issued from holdingbranch
1214     set_userenv($holdingbranch);
1215     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1216     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1217     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1218     ## Can be issued from another branch
1219     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1220     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1221     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1222
1223     # AllowReturnToBranch == holdingbranch
1224     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1225     ## Cannot be issued from homebranch
1226     set_userenv($homebranch);
1227     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1228     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1229     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1230     is( $error->{branch_to_return},         $holdingbranch->{branchcode} );
1231     ## Can be issued from holdinbranch
1232     set_userenv($holdingbranch);
1233     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1234     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1235     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1236     ## Cannot be issued from another branch
1237     set_userenv($otherbranch);
1238     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1239     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1240     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1241     is( $error->{branch_to_return},         $holdingbranch->{branchcode} );
1242
1243     # AllowReturnToBranch == homebranch
1244     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1245     ## Can be issued from holdinbranch
1246     set_userenv($homebranch);
1247     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1248     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1249     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1250     ## Cannot be issued from holdinbranch
1251     set_userenv($holdingbranch);
1252     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1253     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1254     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1255     is( $error->{branch_to_return},         $homebranch->{branchcode} );
1256     ## Cannot be issued from holdinbranch
1257     set_userenv($otherbranch);
1258     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1259     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1260     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1261     is( $error->{branch_to_return},         $homebranch->{branchcode} );
1262
1263     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1264 };
1265
1266 subtest 'AddIssue & AllowReturnToBranch' => sub {
1267     plan tests => 9;
1268
1269     my $homebranch    = $builder->build( { source => 'Branch' } );
1270     my $holdingbranch = $builder->build( { source => 'Branch' } );
1271     my $otherbranch   = $builder->build( { source => 'Branch' } );
1272     my $patron_1      = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1273     my $patron_2      = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1274
1275     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1276     my $item = $builder->build(
1277         {   source => 'Item',
1278             value  => {
1279                 homebranch    => $homebranch->{branchcode},
1280                 holdingbranch => $holdingbranch->{branchcode},
1281                 notforloan    => 0,
1282                 itemlost      => 0,
1283                 withdrawn     => 0,
1284                 biblionumber  => $biblioitem->{biblionumber}
1285             }
1286         }
1287     );
1288
1289     set_userenv($holdingbranch);
1290
1291     my $ref_issue = 'Koha::Checkout';
1292     my $issue = AddIssue( $patron_1, $item->{barcode} );
1293
1294     my ( $error, $question, $alerts );
1295
1296     # AllowReturnToBranch == homebranch
1297     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1298     ## Can be issued from homebranch
1299     set_userenv($homebranch);
1300     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1301     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1302     ## Can be issued from holdinbranch
1303     set_userenv($holdingbranch);
1304     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1305     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1306     ## Can be issued from another branch
1307     set_userenv($otherbranch);
1308     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1309     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1310
1311     # AllowReturnToBranch == holdinbranch
1312     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1313     ## Cannot be issued from homebranch
1314     set_userenv($homebranch);
1315     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1316     ## Can be issued from holdingbranch
1317     set_userenv($holdingbranch);
1318     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1319     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1320     ## Cannot be issued from another branch
1321     set_userenv($otherbranch);
1322     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1323
1324     # AllowReturnToBranch == homebranch
1325     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1326     ## Can be issued from homebranch
1327     set_userenv($homebranch);
1328     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1329     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1330     ## Cannot be issued from holdinbranch
1331     set_userenv($holdingbranch);
1332     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1333     ## Cannot be issued from another branch
1334     set_userenv($otherbranch);
1335     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1336     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1337 };
1338
1339 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1340     plan tests => 8;
1341
1342     my $library = $builder->build( { source => 'Branch' } );
1343     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1344
1345     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1346     my $item_1 = $builder->build(
1347         {   source => 'Item',
1348             value  => {
1349                 homebranch    => $library->{branchcode},
1350                 holdingbranch => $library->{branchcode},
1351                 biblionumber  => $biblioitem_1->{biblionumber}
1352             }
1353         }
1354     );
1355     my $biblioitem_2 = $builder->build( { source => 'Biblioitem' } );
1356     my $item_2 = $builder->build(
1357         {   source => 'Item',
1358             value  => {
1359                 homebranch    => $library->{branchcode},
1360                 holdingbranch => $library->{branchcode},
1361                 biblionumber  => $biblioitem_2->{biblionumber}
1362             }
1363         }
1364     );
1365
1366     my ( $error, $question, $alerts );
1367
1368     # Patron cannot issue item_1, they have overdues
1369     my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1370     my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, $yesterday );    # Add an overdue
1371
1372     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1373     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1374     is( keys(%$error) + keys(%$alerts),  0, 'No key for error and alert' . str($error, $question, $alerts) );
1375     is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
1376
1377     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1378     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1379     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1380     is( $error->{USERBLOCKEDOVERDUE},      1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
1381
1382     # Patron cannot issue item_1, they are debarred
1383     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1384     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
1385     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1386     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1387     is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
1388
1389     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
1390     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1391     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1392     is( $error->{USERBLOCKEDNOENDDATE},    '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
1393 };
1394
1395 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
1396     plan tests => 1;
1397
1398     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1399     my $patron_category_x = $builder->build_object(
1400         {
1401             class => 'Koha::Patron::Categories',
1402             value => { category_type => 'X' }
1403         }
1404     );
1405     my $patron = $builder->build_object(
1406         {
1407             class => 'Koha::Patrons',
1408             value => {
1409                 categorycode  => $patron_category_x->categorycode,
1410                 gonenoaddress => undef,
1411                 lost          => undef,
1412                 debarred      => undef,
1413                 borrowernotes => ""
1414             }
1415         }
1416     );
1417     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1418     my $item_1 = $builder->build(
1419         {
1420             source => 'Item',
1421             value  => {
1422                 homebranch    => $library->branchcode,
1423                 holdingbranch => $library->branchcode,
1424                 biblionumber  => $biblioitem_1->{biblionumber}
1425             }
1426         }
1427     );
1428
1429     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->{barcode} );
1430     is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
1431
1432     # TODO There are other tests to provide here
1433 };
1434
1435 subtest 'MultipleReserves' => sub {
1436     plan tests => 3;
1437
1438     my $biblio = $builder->build_sample_biblio();
1439
1440     my $branch = $library2->{branchcode};
1441
1442     my $item_1 = $builder->build_sample_item(
1443         {
1444             biblionumber     => $biblio->biblionumber,
1445             library          => $branch,
1446             replacementprice => 12.00,
1447             itype            => $itemtype,
1448         }
1449     );
1450
1451     my $item_2 = $builder->build_sample_item(
1452         {
1453             biblionumber     => $biblio->biblionumber,
1454             library          => $branch,
1455             replacementprice => 12.00,
1456             itype            => $itemtype,
1457         }
1458     );
1459
1460     my $bibitems       = '';
1461     my $priority       = '1';
1462     my $resdate        = undef;
1463     my $expdate        = undef;
1464     my $notes          = '';
1465     my $checkitem      = undef;
1466     my $found          = undef;
1467
1468     my %renewing_borrower_data = (
1469         firstname =>  'John',
1470         surname => 'Renewal',
1471         categorycode => $patron_category->{categorycode},
1472         branchcode => $branch,
1473     );
1474     my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
1475     my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
1476     my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
1477     my $datedue = dt_from_string( $issue->date_due() );
1478     is (defined $issue->date_due(), 1, "item 1 checked out");
1479     my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
1480
1481     my %reserving_borrower_data1 = (
1482         firstname =>  'Katrin',
1483         surname => 'Reservation',
1484         categorycode => $patron_category->{categorycode},
1485         branchcode => $branch,
1486     );
1487     my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
1488     AddReserve(
1489         $branch, $reserving_borrowernumber1, $biblio->biblionumber,
1490         $bibitems,  $priority, $resdate, $expdate, $notes,
1491         'a title', $checkitem, $found
1492     );
1493
1494     my %reserving_borrower_data2 = (
1495         firstname =>  'Kirk',
1496         surname => 'Reservation',
1497         categorycode => $patron_category->{categorycode},
1498         branchcode => $branch,
1499     );
1500     my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
1501     AddReserve(
1502         $branch, $reserving_borrowernumber2, $biblio->biblionumber,
1503         $bibitems,  $priority, $resdate, $expdate, $notes,
1504         'a title', $checkitem, $found
1505     );
1506
1507     {
1508         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1509         is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
1510     }
1511
1512     my $item_3 = $builder->build_sample_item(
1513         {
1514             biblionumber     => $biblio->biblionumber,
1515             library          => $branch,
1516             replacementprice => 12.00,
1517             itype            => $itemtype,
1518         }
1519     );
1520
1521     {
1522         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1523         is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
1524     }
1525 };
1526
1527 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
1528     plan tests => 5;
1529
1530     my $library = $builder->build( { source => 'Branch' } );
1531     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1532
1533     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1534     my $biblionumber = $biblioitem->{biblionumber};
1535     my $item_1 = $builder->build(
1536         {   source => 'Item',
1537             value  => {
1538                 homebranch    => $library->{branchcode},
1539                 holdingbranch => $library->{branchcode},
1540                 biblionumber  => $biblionumber,
1541             }
1542         }
1543     );
1544     my $item_2 = $builder->build(
1545         {   source => 'Item',
1546             value  => {
1547                 homebranch    => $library->{branchcode},
1548                 holdingbranch => $library->{branchcode},
1549                 biblionumber  => $biblionumber,
1550             }
1551         }
1552     );
1553
1554     my ( $error, $question, $alerts );
1555     my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, dt_from_string->add( days => 1 ) );
1556
1557     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1558     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1559     is( keys(%$error) + keys(%$alerts),  0, 'No error or alert should be raised' . str($error, $question, $alerts) );
1560     is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' . str($error, $question, $alerts) );
1561
1562     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1563     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1564     is( keys(%$error) + keys(%$question) + keys(%$alerts),  0, 'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1' . str($error, $question, $alerts) );
1565
1566     # Add a subscription
1567     Koha::Subscription->new({ biblionumber => $biblionumber })->store;
1568
1569     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1570     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1571     is( keys(%$error) + keys(%$question) + keys(%$alerts),  0, 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription' . str($error, $question, $alerts) );
1572
1573     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1574     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1575     is( keys(%$error) + keys(%$question) + keys(%$alerts),  0, 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription' . str($error, $question, $alerts) );
1576 };
1577
1578 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
1579     plan tests => 8;
1580
1581     my $library = $builder->build( { source => 'Branch' } );
1582     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1583
1584     # Add 2 items
1585     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1586     my $item_1 = $builder->build(
1587         {
1588             source => 'Item',
1589             value  => {
1590                 homebranch    => $library->{branchcode},
1591                 holdingbranch => $library->{branchcode},
1592                 notforloan    => 0,
1593                 itemlost      => 0,
1594                 withdrawn     => 0,
1595                 biblionumber  => $biblioitem_1->{biblionumber}
1596             }
1597         }
1598     );
1599     my $biblioitem_2 = $builder->build( { source => 'Biblioitem' } );
1600     my $item_2 = $builder->build(
1601         {
1602             source => 'Item',
1603             value  => {
1604                 homebranch    => $library->{branchcode},
1605                 holdingbranch => $library->{branchcode},
1606                 notforloan    => 0,
1607                 itemlost      => 0,
1608                 withdrawn     => 0,
1609                 biblionumber  => $biblioitem_2->{biblionumber}
1610             }
1611         }
1612     );
1613
1614     # And the issuing rule
1615     Koha::IssuingRules->search->delete;
1616     my $rule = Koha::IssuingRule->new(
1617         {
1618             categorycode => '*',
1619             itemtype     => '*',
1620             branchcode   => '*',
1621             issuelength  => 1,
1622             firstremind  => 1,        # 1 day of grace
1623             finedays     => 2,        # 2 days of fine per day of overdue
1624             lengthunit   => 'days',
1625         }
1626     );
1627     $rule->store();
1628
1629     # Patron cannot issue item_1, they have overdues
1630     my $five_days_ago = dt_from_string->subtract( days => 5 );
1631     my $ten_days_ago  = dt_from_string->subtract( days => 10 );
1632     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
1633     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
1634       ;    # Add another overdue
1635
1636     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
1637     AddReturn( $item_1->{barcode}, $library->{branchcode},
1638         undef, undef, dt_from_string );
1639     my $debarments = Koha::Patron::Debarments::GetDebarments(
1640         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1641     is( scalar(@$debarments), 1 );
1642
1643     # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
1644     # Same for the others
1645     my $expected_expiration = output_pref(
1646         {
1647             dt         => dt_from_string->add( days => ( 5 - 1 ) * 2 ),
1648             dateformat => 'sql',
1649             dateonly   => 1
1650         }
1651     );
1652     is( $debarments->[0]->{expiration}, $expected_expiration );
1653
1654     AddReturn( $item_2->{barcode}, $library->{branchcode},
1655         undef, undef, dt_from_string );
1656     $debarments = Koha::Patron::Debarments::GetDebarments(
1657         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1658     is( scalar(@$debarments), 1 );
1659     $expected_expiration = output_pref(
1660         {
1661             dt         => dt_from_string->add( days => ( 10 - 1 ) * 2 ),
1662             dateformat => 'sql',
1663             dateonly   => 1
1664         }
1665     );
1666     is( $debarments->[0]->{expiration}, $expected_expiration );
1667
1668     Koha::Patron::Debarments::DelUniqueDebarment(
1669         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1670
1671     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
1672     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
1673     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
1674       ;    # Add another overdue
1675     AddReturn( $item_1->{barcode}, $library->{branchcode},
1676         undef, undef, dt_from_string );
1677     $debarments = Koha::Patron::Debarments::GetDebarments(
1678         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1679     is( scalar(@$debarments), 1 );
1680     $expected_expiration = output_pref(
1681         {
1682             dt         => dt_from_string->add( days => ( 5 - 1 ) * 2 ),
1683             dateformat => 'sql',
1684             dateonly   => 1
1685         }
1686     );
1687     is( $debarments->[0]->{expiration}, $expected_expiration );
1688
1689     AddReturn( $item_2->{barcode}, $library->{branchcode},
1690         undef, undef, dt_from_string );
1691     $debarments = Koha::Patron::Debarments::GetDebarments(
1692         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1693     is( scalar(@$debarments), 1 );
1694     $expected_expiration = output_pref(
1695         {
1696             dt => dt_from_string->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
1697             dateformat => 'sql',
1698             dateonly   => 1
1699         }
1700     );
1701     is( $debarments->[0]->{expiration}, $expected_expiration );
1702 };
1703
1704 subtest 'AddReturn + suspension_chargeperiod' => sub {
1705     plan tests => 21;
1706
1707     my $library = $builder->build( { source => 'Branch' } );
1708     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1709
1710     # Add 2 items
1711     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1712     my $item_1 = $builder->build(
1713         {
1714             source => 'Item',
1715             value  => {
1716                 homebranch    => $library->{branchcode},
1717                 holdingbranch => $library->{branchcode},
1718                 notforloan    => 0,
1719                 itemlost      => 0,
1720                 withdrawn     => 0,
1721                 biblionumber  => $biblioitem_1->{biblionumber}
1722             }
1723         }
1724     );
1725
1726     # And the issuing rule
1727     Koha::IssuingRules->search->delete;
1728     my $rule = Koha::IssuingRule->new(
1729         {
1730             categorycode => '*',
1731             itemtype     => '*',
1732             branchcode   => '*',
1733             maxissueqty  => 99,
1734             issuelength  => 1,
1735             firstremind  => 0,        # 0 day of grace
1736             finedays     => 2,        # 2 days of fine per day of overdue
1737             suspension_chargeperiod => 1,
1738             lengthunit   => 'days',
1739         }
1740     );
1741     $rule->store();
1742
1743     my $five_days_ago = dt_from_string->subtract( days => 5 );
1744     # We want to charge 2 days every day, without grace
1745     # With 5 days of overdue: 5 * Z
1746     my $expected_expiration = dt_from_string->add( days => ( 5 * 2 ) / 1 );
1747     test_debarment_on_checkout(
1748         {
1749             item            => $item_1,
1750             library         => $library,
1751             patron          => $patron,
1752             due_date        => $five_days_ago,
1753             expiration_date => $expected_expiration,
1754         }
1755     );
1756
1757     # We want to charge 2 days every 2 days, without grace
1758     # With 5 days of overdue: (5 * 2) / 2
1759     $rule->suspension_chargeperiod(2)->store;
1760     $expected_expiration = dt_from_string->add( days => floor( 5 * 2 ) / 2 );
1761     test_debarment_on_checkout(
1762         {
1763             item            => $item_1,
1764             library         => $library,
1765             patron          => $patron,
1766             due_date        => $five_days_ago,
1767             expiration_date => $expected_expiration,
1768         }
1769     );
1770
1771     # We want to charge 2 days every 3 days, with 1 day of grace
1772     # With 5 days of overdue: ((5-1) / 3 ) * 2
1773     $rule->suspension_chargeperiod(3)->store;
1774     $rule->firstremind(1)->store;
1775     $expected_expiration = dt_from_string->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
1776     test_debarment_on_checkout(
1777         {
1778             item            => $item_1,
1779             library         => $library,
1780             patron          => $patron,
1781             due_date        => $five_days_ago,
1782             expiration_date => $expected_expiration,
1783         }
1784     );
1785
1786     # Use finesCalendar to know if holiday must be skipped to calculate the due date
1787     # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
1788     $rule->finedays(2)->store;
1789     $rule->suspension_chargeperiod(1)->store;
1790     $rule->firstremind(0)->store;
1791     t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
1792
1793     # Adding a holiday 2 days ago
1794     my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
1795     my $two_days_ago = dt_from_string->subtract( days => 2 );
1796     $calendar->insert_single_holiday(
1797         day             => $two_days_ago->day,
1798         month           => $two_days_ago->month,
1799         year            => $two_days_ago->year,
1800         title           => 'holidayTest-2d',
1801         description     => 'holidayDesc 2 days ago'
1802     );
1803     # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
1804     $expected_expiration = dt_from_string->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
1805     test_debarment_on_checkout(
1806         {
1807             item            => $item_1,
1808             library         => $library,
1809             patron          => $patron,
1810             due_date        => $five_days_ago,
1811             expiration_date => $expected_expiration,
1812         }
1813     );
1814
1815     # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
1816     my $two_days_ahead = dt_from_string->add( days => 2 );
1817     $calendar->insert_single_holiday(
1818         day             => $two_days_ahead->day,
1819         month           => $two_days_ahead->month,
1820         year            => $two_days_ahead->year,
1821         title           => 'holidayTest+2d',
1822         description     => 'holidayDesc 2 days ahead'
1823     );
1824
1825     # Same as above, but we should skip D+2
1826     $expected_expiration = dt_from_string->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
1827     test_debarment_on_checkout(
1828         {
1829             item            => $item_1,
1830             library         => $library,
1831             patron          => $patron,
1832             due_date        => $five_days_ago,
1833             expiration_date => $expected_expiration,
1834         }
1835     );
1836
1837     # Adding another holiday, day of expiration date
1838     my $expected_expiration_dt = dt_from_string($expected_expiration);
1839     $calendar->insert_single_holiday(
1840         day             => $expected_expiration_dt->day,
1841         month           => $expected_expiration_dt->month,
1842         year            => $expected_expiration_dt->year,
1843         title           => 'holidayTest_exp',
1844         description     => 'holidayDesc on expiration date'
1845     );
1846     # Expiration date will be the day after
1847     test_debarment_on_checkout(
1848         {
1849             item            => $item_1,
1850             library         => $library,
1851             patron          => $patron,
1852             due_date        => $five_days_ago,
1853             expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
1854         }
1855     );
1856
1857     test_debarment_on_checkout(
1858         {
1859             item            => $item_1,
1860             library         => $library,
1861             patron          => $patron,
1862             return_date     => dt_from_string->add(days => 5),
1863             expiration_date => dt_from_string->add(days => 5 + (5 * 2 - 1) ),
1864         }
1865     );
1866 };
1867
1868 subtest 'AddReturn | is_overdue' => sub {
1869     plan tests => 5;
1870
1871     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
1872     t::lib::Mocks::mock_preference('finesMode', 'production');
1873     t::lib::Mocks::mock_preference('MaxFine', '100');
1874
1875     my $library = $builder->build( { source => 'Branch' } );
1876     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1877
1878     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1879     my $item = $builder->build(
1880         {
1881             source => 'Item',
1882             value  => {
1883                 homebranch    => $library->{branchcode},
1884                 holdingbranch => $library->{branchcode},
1885                 notforloan    => 0,
1886                 itemlost      => 0,
1887                 withdrawn     => 0,
1888                 biblionumber  => $biblioitem->{biblionumber},
1889             }
1890         }
1891     );
1892
1893     Koha::IssuingRules->search->delete;
1894     my $rule = Koha::IssuingRule->new(
1895         {
1896             categorycode => '*',
1897             itemtype     => '*',
1898             branchcode   => '*',
1899             maxissueqty  => 99,
1900             issuelength  => 6,
1901             lengthunit   => 'days',
1902             fine         => 1, # Charge 1 every day of overdue
1903             chargeperiod => 1,
1904         }
1905     );
1906     $rule->store();
1907
1908     my $one_day_ago   = dt_from_string->subtract( days => 1 );
1909     my $five_days_ago = dt_from_string->subtract( days => 5 );
1910     my $ten_days_ago  = dt_from_string->subtract( days => 10 );
1911     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
1912
1913     # No date specify, today will be used
1914     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
1915     AddReturn( $item->{barcode}, $library->{branchcode} );
1916     is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
1917     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
1918
1919     # specify return date 5 days before => no overdue
1920     AddIssue( $patron->unblessed, $item->{barcode}, $five_days_ago ); # date due was 5d ago
1921     AddReturn( $item->{barcode}, $library->{branchcode}, undef, undef, $ten_days_ago );
1922     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
1923     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
1924
1925     # specify return date 5 days later => overdue
1926     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
1927     AddReturn( $item->{barcode}, $library->{branchcode}, undef, undef, $five_days_ago );
1928     is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
1929     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
1930
1931     # specify dropbox date 5 days before => no overdue
1932     AddIssue( $patron->unblessed, $item->{barcode}, $five_days_ago ); # date due was 5d ago
1933     AddReturn( $item->{barcode}, $library->{branchcode}, undef, 1, undef, $ten_days_ago );
1934     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
1935     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
1936
1937     # specify dropbox date 5 days later => overdue, or... not
1938     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
1939     AddReturn( $item->{barcode}, $library->{branchcode}, undef, 1, undef, $five_days_ago );
1940     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue in dropbox mode' ); # FIXME? This is weird, the FU fine is created ( _CalculateAndUpdateFine > C4::Overdues::UpdateFine ) then remove later (in _FixOverduesOnReturn). Looks like it is a feature
1941     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
1942 };
1943
1944 subtest '_FixAccountForLostAndReturned' => sub {
1945
1946     plan tests => 5;
1947
1948     t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
1949     t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
1950
1951     my $processfee_amount  = 20;
1952     my $replacement_amount = 99.00;
1953     my $item_type          = $builder->build_object(
1954         {   class => 'Koha::ItemTypes',
1955             value => {
1956                 notforloan         => undef,
1957                 rentalcharge       => 0,
1958                 defaultreplacecost => undef,
1959                 processfee         => $processfee_amount
1960             }
1961         }
1962     );
1963     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1964
1965     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Daria' });
1966
1967     subtest 'Full write-off tests' => sub {
1968
1969         plan tests => 10;
1970
1971         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1972
1973         my $item = $builder->build_sample_item(
1974             {
1975                 biblionumber     => $biblio->biblionumber,
1976                 library          => $library->branchcode,
1977                 replacementprice => $replacement_amount,
1978                 itype            => $item_type->itemtype,
1979             }
1980         );
1981
1982         AddIssue( $patron->unblessed, $item->barcode );
1983
1984         # Simulate item marked as lost
1985         ModItem( { itemlost => 3 }, $biblio->biblionumber, $item->itemnumber );
1986         LostItem( $item->itemnumber, 1 );
1987
1988         my $processing_fee_lines = Koha::Account::Lines->search(
1989             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
1990         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
1991         my $processing_fee_line = $processing_fee_lines->next;
1992         is( $processing_fee_line->amount + 0,
1993             $processfee_amount, 'The right PF amount is generated' );
1994         is( $processing_fee_line->amountoutstanding + 0,
1995             $processfee_amount, 'The right PF amountoutstanding is generated' );
1996
1997         my $lost_fee_lines = Koha::Account::Lines->search(
1998             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
1999         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2000         my $lost_fee_line = $lost_fee_lines->next;
2001         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2002         is( $lost_fee_line->amountoutstanding + 0,
2003             $replacement_amount, 'The right L amountoutstanding is generated' );
2004
2005         my $account = $patron->account;
2006         my $debts   = $account->outstanding_debits;
2007
2008         # Write off the debt
2009         my $credit = $account->add_credit(
2010             {   amount => $account->balance,
2011                 type   => 'writeoff'
2012             }
2013         );
2014         $credit->apply( { debits => $debts, offset_type => 'Writeoff' } );
2015
2016         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2017         is( $credit_return_id, undef, 'No CR account line added' );
2018
2019         $lost_fee_line->discard_changes; # reload from DB
2020         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2021         is( $lost_fee_line->accounttype,
2022             'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2023
2024         is( $patron->account->balance, -0, 'The patron balance is 0, everything was written off' );
2025     };
2026
2027     subtest 'Full payment tests' => sub {
2028
2029         plan tests => 12;
2030
2031         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2032
2033         my $item = $builder->build_sample_item(
2034             {
2035                 biblionumber     => $biblio->biblionumber,
2036                 library          => $library->branchcode,
2037                 replacementprice => $replacement_amount,
2038                 itype            => $item_type->itemtype
2039             }
2040         );
2041
2042         AddIssue( $patron->unblessed, $item->barcode );
2043
2044         # Simulate item marked as lost
2045         ModItem( { itemlost => 1 }, $biblio->biblionumber, $item->itemnumber );
2046         LostItem( $item->itemnumber, 1 );
2047
2048         my $processing_fee_lines = Koha::Account::Lines->search(
2049             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
2050         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2051         my $processing_fee_line = $processing_fee_lines->next;
2052         is( $processing_fee_line->amount + 0,
2053             $processfee_amount, 'The right PF amount is generated' );
2054         is( $processing_fee_line->amountoutstanding + 0,
2055             $processfee_amount, 'The right PF amountoutstanding is generated' );
2056
2057         my $lost_fee_lines = Koha::Account::Lines->search(
2058             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
2059         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2060         my $lost_fee_line = $lost_fee_lines->next;
2061         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2062         is( $lost_fee_line->amountoutstanding + 0,
2063             $replacement_amount, 'The right L amountountstanding is generated' );
2064
2065         my $account = $patron->account;
2066         my $debts   = $account->outstanding_debits;
2067
2068         # Write off the debt
2069         my $credit = $account->add_credit(
2070             {   amount => $account->balance,
2071                 type   => 'payment'
2072             }
2073         );
2074         $credit->apply( { debits => $debts, offset_type => 'Payment' } );
2075
2076         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2077         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2078
2079         is( $credit_return->accounttype, 'CR', 'An account line of type CR is added' );
2080         is( $credit_return->amount + 0,
2081             -99.00, 'The account line of type CR has an amount of -99' );
2082         is( $credit_return->amountoutstanding + 0,
2083             -99.00, 'The account line of type CR has an amountoutstanding of -99' );
2084
2085         $lost_fee_line->discard_changes;
2086         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2087         is( $lost_fee_line->accounttype,
2088             'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2089
2090         is( $patron->account->balance,
2091             -99, 'The patron balance is -99, a credit that equals the lost fee payment' );
2092     };
2093
2094     subtest 'Test without payment or write off' => sub {
2095
2096         plan tests => 12;
2097
2098         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2099
2100         my $item = $builder->build_sample_item(
2101             {
2102                 biblionumber     => $biblio->biblionumber,
2103                 library          => $library->branchcode,
2104                 replacementprice => 23.00,
2105                 replacementprice => $replacement_amount,
2106                 itype            => $item_type->itemtype
2107             }
2108         );
2109
2110         AddIssue( $patron->unblessed, $item->barcode );
2111
2112         # Simulate item marked as lost
2113         ModItem( { itemlost => 3 }, $biblio->biblionumber, $item->itemnumber );
2114         LostItem( $item->itemnumber, 1 );
2115
2116         my $processing_fee_lines = Koha::Account::Lines->search(
2117             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
2118         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2119         my $processing_fee_line = $processing_fee_lines->next;
2120         is( $processing_fee_line->amount + 0,
2121             $processfee_amount, 'The right PF amount is generated' );
2122         is( $processing_fee_line->amountoutstanding + 0,
2123             $processfee_amount, 'The right PF amountoutstanding is generated' );
2124
2125         my $lost_fee_lines = Koha::Account::Lines->search(
2126             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
2127         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2128         my $lost_fee_line = $lost_fee_lines->next;
2129         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2130         is( $lost_fee_line->amountoutstanding + 0,
2131             $replacement_amount, 'The right L amountountstanding is generated' );
2132
2133         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2134         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2135
2136         is( $credit_return->accounttype, 'CR', 'An account line of type CR is added' );
2137         is( $credit_return->amount + 0, -99.00, 'The account line of type CR has an amount of -99' );
2138         is( $credit_return->amountoutstanding + 0, 0, 'The account line of type CR has an amountoutstanding of 0' );
2139
2140         $lost_fee_line->discard_changes;
2141         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2142         is( $lost_fee_line->accounttype, 'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2143
2144         is( $patron->account->balance, 20, 'The patron balance is 20, still owes the processing fee' );
2145     };
2146
2147     subtest 'Test with partial payement and write off, and remaining debt' => sub {
2148
2149         plan tests => 15;
2150
2151         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2152         my $item = $builder->build_sample_item(
2153             {
2154                 biblionumber     => $biblio->biblionumber,
2155                 library          => $library->branchcode,
2156                 replacementprice => $replacement_amount,
2157                 itype            => $item_type->itemtype
2158             }
2159         );
2160
2161         AddIssue( $patron->unblessed, $item->barcode );
2162
2163         # Simulate item marked as lost
2164         ModItem( { itemlost => 1 }, $biblio->biblionumber, $item->itemnumber );
2165         LostItem( $item->itemnumber, 1 );
2166
2167         my $processing_fee_lines = Koha::Account::Lines->search(
2168             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
2169         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2170         my $processing_fee_line = $processing_fee_lines->next;
2171         is( $processing_fee_line->amount + 0,
2172             $processfee_amount, 'The right PF amount is generated' );
2173         is( $processing_fee_line->amountoutstanding + 0,
2174             $processfee_amount, 'The right PF amountoutstanding is generated' );
2175
2176         my $lost_fee_lines = Koha::Account::Lines->search(
2177             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
2178         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2179         my $lost_fee_line = $lost_fee_lines->next;
2180         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2181         is( $lost_fee_line->amountoutstanding + 0,
2182             $replacement_amount, 'The right L amountountstanding is generated' );
2183
2184         my $account = $patron->account;
2185         is( $account->balance, $processfee_amount + $replacement_amount, 'Balance is PF + L' );
2186
2187         # Partially pay fee
2188         my $payment_amount = 27;
2189         my $payment        = $account->add_credit(
2190             {   amount => $payment_amount,
2191                 type   => 'payment'
2192             }
2193         );
2194
2195         $payment->apply( { debits => $lost_fee_lines->reset, offset_type => 'Payment' } );
2196
2197         # Partially write off fee
2198         my $write_off_amount = 25;
2199         my $write_off        = $account->add_credit(
2200             {   amount => $write_off_amount,
2201                 type   => 'writeoff'
2202             }
2203         );
2204         $write_off->apply( { debits => $lost_fee_lines->reset, offset_type => 'Writeoff' } );
2205
2206         is( $account->balance,
2207             $processfee_amount + $replacement_amount - $payment_amount - $write_off_amount,
2208             'Payment and write off applied'
2209         );
2210
2211         # Store the amountoutstanding value
2212         $lost_fee_line->discard_changes;
2213         my $outstanding = $lost_fee_line->amountoutstanding;
2214
2215         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2216         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2217
2218         is( $account->balance, $processfee_amount - $payment_amount, 'Balance is PF - payment (CR)' );
2219
2220         $lost_fee_line->discard_changes;
2221         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2222         is( $lost_fee_line->accounttype,
2223             'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2224
2225         is( $credit_return->accounttype, 'CR', 'An account line of type CR is added' );
2226         is( $credit_return->amount + 0,
2227             ($payment_amount + $outstanding ) * -1,
2228             'The account line of type CR has an amount equal to the payment + outstanding'
2229         );
2230         is( $credit_return->amountoutstanding + 0,
2231             $payment_amount * -1,
2232             'The account line of type CR has an amountoutstanding equal to the payment'
2233         );
2234
2235         is( $account->balance,
2236             $processfee_amount - $payment_amount,
2237             'The patron balance is the difference between the PF and the credit'
2238         );
2239     };
2240
2241     subtest 'Partial payement, existing debits and AccountAutoReconcile' => sub {
2242
2243         plan tests => 8;
2244
2245         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2246         my $barcode = 'KD123456793';
2247         my $replacement_amount = 100;
2248         my $processfee_amount  = 20;
2249
2250         my $item_type          = $builder->build_object(
2251             {   class => 'Koha::ItemTypes',
2252                 value => {
2253                     notforloan         => undef,
2254                     rentalcharge       => 0,
2255                     defaultreplacecost => undef,
2256                     processfee         => 0
2257                 }
2258             }
2259         );
2260         my ( undef, undef, $item_id ) = AddItem(
2261             {   homebranch       => $library->branchcode,
2262                 holdingbranch    => $library->branchcode,
2263                 barcode          => $barcode,
2264                 replacementprice => $replacement_amount,
2265                 itype            => $item_type->itemtype
2266             },
2267             $biblio->biblionumber
2268         );
2269
2270         AddIssue( $patron->unblessed, $barcode );
2271
2272         # Simulate item marked as lost
2273         ModItem( { itemlost => 1 }, $biblio->biblionumber, $item_id );
2274         LostItem( $item_id, 1 );
2275
2276         my $lost_fee_lines = Koha::Account::Lines->search(
2277             { borrowernumber => $patron->id, itemnumber => $item_id, accounttype => 'L' } );
2278         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2279         my $lost_fee_line = $lost_fee_lines->next;
2280         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2281         is( $lost_fee_line->amountoutstanding + 0,
2282             $replacement_amount, 'The right L amountountstanding is generated' );
2283
2284         my $account = $patron->account;
2285         is( $account->balance, $replacement_amount, 'Balance is L' );
2286
2287         # Partially pay fee
2288         my $payment_amount = 27;
2289         my $payment        = $account->add_credit(
2290             {   amount => $payment_amount,
2291                 type   => 'payment'
2292             }
2293         );
2294         $payment->apply({ debits => $lost_fee_lines->reset, offset_type => 'Payment' });
2295
2296         is( $account->balance,
2297             $replacement_amount - $payment_amount,
2298             'Payment applied'
2299         );
2300
2301         # TODO use add_debit when time comes
2302         my $manual_debit_amount = 80;
2303         C4::Accounts::manualinvoice( $patron->id, undef, undef, 'FU', $manual_debit_amount );
2304
2305         is( $account->balance, $manual_debit_amount + $replacement_amount - $payment_amount, 'Manual debit applied' );
2306
2307         t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
2308
2309         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item_id, $patron->id );
2310         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2311
2312         is( $account->balance, $manual_debit_amount - $payment_amount, 'Balance is PF - payment (CR)' );
2313
2314         my $manual_debit = Koha::Account::Lines->search({ borrowernumber => $patron->id, accounttype => 'FU' })->next;
2315         is( $manual_debit->amountoutstanding + 0, $manual_debit_amount - $payment_amount, 'reconcile_balance was called' );
2316     };
2317 };
2318
2319 subtest '_FixOverduesOnReturn' => sub {
2320     plan tests => 10;
2321
2322     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
2323
2324     my $branchcode  = $library2->{branchcode};
2325
2326     my $item = $builder->build_sample_item(
2327         {
2328             biblionumber     => $biblio->biblionumber,
2329             library          => $branchcode,
2330             replacementprice => 99.00,
2331             itype            => $itemtype,
2332         }
2333     );
2334
2335     my $patron = $builder->build( { source => 'Borrower' } );
2336
2337     ## Start with basic call, should just close out the open fine
2338     my $accountline = Koha::Account::Line->new(
2339         {
2340             borrowernumber => $patron->{borrowernumber},
2341             accounttype    => 'FU',
2342             itemnumber     => $item->itemnumber,
2343             amount => 99.00,
2344             amountoutstanding => 99.00,
2345             lastincrement => 9.00,
2346         }
2347     )->store();
2348
2349     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber );
2350
2351     $accountline->_result()->discard_changes();
2352
2353     is( $accountline->amountoutstanding, '99.000000', 'Fine has the same amount outstanding as previously' );
2354     is( $accountline->accounttype, 'F', 'Open fine ( account type FU ) has been closed out ( account type F )');
2355
2356
2357     ## Run again, with exemptfine enabled
2358     $accountline->set(
2359         {
2360             accounttype    => 'FU',
2361             amountoutstanding => 99.00,
2362         }
2363     )->store();
2364
2365     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1 );
2366
2367     $accountline->_result()->discard_changes();
2368     my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
2369
2370     is( $accountline->amountoutstanding + 0, 0, 'Fine has been reduced to 0' );
2371     is( $accountline->accounttype, 'FFOR', 'Open fine ( account type FU ) has been set to fine forgiven ( account type FFOR )');
2372     is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
2373     is( $offset->amount, '-99.000000', "Amount of offset is correct" );
2374
2375     ## Run again, with dropbox mode enabled
2376     $accountline->set(
2377         {
2378             accounttype    => 'FU',
2379             amountoutstanding => 99.00,
2380         }
2381     )->store();
2382
2383     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 0, 1 );
2384
2385     $accountline->_result()->discard_changes();
2386     $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Dropbox' })->next();
2387
2388     is( $accountline->amountoutstanding + 0, 90, 'Fine has been reduced to 90' );
2389     is( $accountline->accounttype, 'F', 'Open fine ( account type FU ) has been closed out ( account type F )');
2390     is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via dropbox" );
2391     is( $offset->amount, '-9.000000', "Amount of offset is correct" );
2392 };
2393
2394 subtest 'Set waiting flag' => sub {
2395     plan tests => 4;
2396
2397     my $library_1 = $builder->build( { source => 'Branch' } );
2398     my $patron_1  = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2399     my $library_2 = $builder->build( { source => 'Branch' } );
2400     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2401
2402     my $biblio = $builder->build( { source => 'Biblio' } );
2403     my $biblioitem = $builder->build( { source => 'Biblioitem', value => { biblionumber => $biblio->{biblionumber} } } );
2404
2405     my $item = $builder->build(
2406         {
2407             source => 'Item',
2408             value  => {
2409                 homebranch    => $library_1->{branchcode},
2410                 holdingbranch => $library_1->{branchcode},
2411                 notforloan    => 0,
2412                 itemlost      => 0,
2413                 withdrawn     => 0,
2414                 biblionumber  => $biblioitem->{biblionumber},
2415             }
2416         }
2417     );
2418
2419     set_userenv( $library_2 );
2420     my $reserve_id = AddReserve(
2421         $library_2->{branchcode}, $patron_2->{borrowernumber}, $biblioitem->{biblionumber},
2422         '', 1, undef, undef, '', undef, $item->{itemnumber},
2423     );
2424
2425     set_userenv( $library_1 );
2426     my $do_transfer = 1;
2427     my ( $res, $rr ) = AddReturn( $item->{barcode}, $library_1->{branchcode} );
2428     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
2429     my $hold = Koha::Holds->find( $reserve_id );
2430     is( $hold->found, 'T', 'Hold is in transit' );
2431
2432     my ( $status ) = CheckReserves($item->{itemnumber});
2433     is( $status, 'Reserved', 'Hold is not waiting yet');
2434
2435     set_userenv( $library_2 );
2436     $do_transfer = 0;
2437     AddReturn( $item->{barcode}, $library_2->{branchcode} );
2438     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
2439     $hold = Koha::Holds->find( $reserve_id );
2440     is( $hold->found, 'W', 'Hold is waiting' );
2441     ( $status ) = CheckReserves($item->{itemnumber});
2442     is( $status, 'Waiting', 'Now the hold is waiting');
2443 };
2444
2445 subtest 'Cancel transfers on lost items' => sub {
2446     plan tests => 5;
2447     my $library_1 = $builder->build( { source => 'Branch' } );
2448     my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2449     my $library_2 = $builder->build( { source => 'Branch' } );
2450     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2451     my $biblio = $builder->build( { source => 'Biblio' } );
2452     my $biblioitem = $builder->build( { source => 'Biblioitem', value => { biblionumber => $biblio->{biblionumber} } } );
2453     my $item = $builder->build(
2454         {
2455             source => 'Item',
2456             value => {
2457                 homebranch => $library_1->{branchcode},
2458                 holdingbranch => $library_1->{branchcode},
2459                 notforloan => 0,
2460                 itemlost => 0,
2461                 withdrawn => 0,
2462                 biblionumber => $biblioitem->{biblionumber},
2463             }
2464         }
2465     );
2466
2467     set_userenv( $library_2 );
2468     my $reserve_id = AddReserve(
2469         $library_2->{branchcode}, $patron_2->{borrowernumber}, $biblioitem->{biblionumber}, '', 1, undef, undef, '', undef, $item->{itemnumber},
2470     );
2471
2472     #Return book and add transfer
2473     set_userenv( $library_1 );
2474     my $do_transfer = 1;
2475     my ( $res, $rr ) = AddReturn( $item->{barcode}, $library_1->{branchcode} );
2476     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
2477     C4::Circulation::transferbook( $library_2->{branchcode}, $item->{barcode} );
2478     my $hold = Koha::Holds->find( $reserve_id );
2479     is( $hold->found, 'T', 'Hold is in transit' );
2480
2481     #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
2482     my ($datesent,$frombranch,$tobranch) = GetTransfers($item->{itemnumber});
2483     is( $tobranch, $library_2->{branchcode}, 'The transfer record exists in the branchtransfers table');
2484     my $itemcheck = Koha::Items->find($item->{itemnumber});
2485     is( $itemcheck->holdingbranch, $library_2->{branchcode}, 'Items holding branch is the transfers destination branch before it is marked as lost' );
2486
2487     #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
2488     ModItem( { itemlost => 1 }, $biblio->{biblionumber}, $item->{itemnumber} );
2489     LostItem( $item->{itemnumber}, 'test', 1 );
2490     ($datesent,$frombranch,$tobranch) = GetTransfers($item->{itemnumber});
2491     is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
2492     $itemcheck = Koha::Items->find($item->{itemnumber});
2493     is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
2494 };
2495
2496 subtest 'CanBookBeIssued | is_overdue' => sub {
2497     plan tests => 3;
2498
2499     # Set a simple circ policy
2500     $dbh->do('DELETE FROM issuingrules');
2501     $dbh->do(
2502     q{INSERT INTO issuingrules (categorycode, branchcode, itemtype, reservesallowed,
2503                                     maxissueqty, issuelength, lengthunit,
2504                                     renewalsallowed, renewalperiod,
2505                                     norenewalbefore, auto_renew,
2506                                     fine, chargeperiod)
2507           VALUES (?, ?, ?, ?,
2508                   ?, ?, ?,
2509                   ?, ?,
2510                   ?, ?,
2511                   ?, ?
2512                  )
2513         },
2514         {},
2515         '*',   '*', '*', 25,
2516         1,     14,  'days',
2517         1,     7,
2518         undef, 0,
2519         .10,   1
2520     );
2521
2522     my $five_days_go = output_pref({ dt => dt_from_string->add( days => 5 ), dateonly => 1});
2523     my $ten_days_go  = output_pref({ dt => dt_from_string->add( days => 10), dateonly => 1 });
2524     my $library = $builder->build( { source => 'Branch' } );
2525     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2526
2527     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
2528     my $item = $builder->build(
2529         {
2530             source => 'Item',
2531             value  => {
2532                 homebranch    => $library->{branchcode},
2533                 holdingbranch => $library->{branchcode},
2534                 notforloan    => 0,
2535                 itemlost      => 0,
2536                 withdrawn     => 0,
2537                 biblionumber  => $biblioitem->{biblionumber},
2538             }
2539         }
2540     );
2541
2542     my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $five_days_go ); # date due was 10d ago
2543     my $actualissue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
2544     is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
2545     my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->{barcode},$ten_days_go, undef, undef, undef);
2546     is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
2547     is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
2548 };
2549
2550 subtest 'ItemsDeniedRenewal preference' => sub {
2551     plan tests => 18;
2552
2553     C4::Context->set_preference('ItemsDeniedRenewal','');
2554
2555     my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
2556     $dbh->do(
2557         q{
2558         INSERT INTO issuingrules ( categorycode, branchcode, itemtype, reservesallowed, maxissueqty, issuelength, lengthunit, renewalsallowed, renewalperiod,
2559                     norenewalbefore, auto_renew, fine, chargeperiod ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2560         },
2561         {},
2562         '*', $idr_lib->branchcode, '*', 25,
2563         20,  14,  'days',
2564         10,   7,
2565         undef,  0,
2566         .10, 1
2567     );
2568
2569     my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
2570         homebranch => $idr_lib->branchcode,
2571         withdrawn => 1,
2572         itype => 'HIDE',
2573         location => 'PROC',
2574         itemcallnumber => undef,
2575         itemnotes => "",
2576         }
2577     });
2578     my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
2579         homebranch => $idr_lib->branchcode,
2580         withdrawn => 0,
2581         itype => 'NOHIDE',
2582         location => 'NOPROC'
2583         }
2584     });
2585
2586     my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
2587         branchcode => $idr_lib->branchcode,
2588         }
2589     });
2590     my $future = dt_from_string->add( days => 1 );
2591     my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
2592         returndate => undef,
2593         renewals => 0,
2594         auto_renew => 0,
2595         borrowernumber => $idr_borrower->borrowernumber,
2596         itemnumber => $deny_book->itemnumber,
2597         onsite_checkout => 0,
2598         date_due => $future,
2599         }
2600     });
2601     my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
2602         returndate => undef,
2603         renewals => 0,
2604         auto_renew => 0,
2605         borrowernumber => $idr_borrower->borrowernumber,
2606         itemnumber => $allow_book->itemnumber,
2607         onsite_checkout => 0,
2608         date_due => $future,
2609         }
2610     });
2611
2612     my $idr_rules;
2613
2614     my ( $idr_mayrenew, $idr_error ) =
2615     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2616     is( $idr_mayrenew, 1, 'Renewal allowed when no rules' );
2617     is( $idr_error, undef, 'Renewal allowed when no rules' );
2618
2619     $idr_rules="withdrawn: [1]";
2620
2621     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2622     ( $idr_mayrenew, $idr_error ) =
2623     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2624     is( $idr_mayrenew, 0, 'Renewal blocked when 1 rules (withdrawn)' );
2625     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 1 rule (withdrawn)' );
2626     ( $idr_mayrenew, $idr_error ) =
2627     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
2628     is( $idr_mayrenew, 1, 'Renewal allowed when 1 rules not matched (withdrawn)' );
2629     is( $idr_error, undef, 'Renewal allowed when 1 rules not matched (withdrawn)' );
2630
2631     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
2632
2633     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2634     ( $idr_mayrenew, $idr_error ) =
2635     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2636     is( $idr_mayrenew, 0, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
2637     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 2 rules matched (withdrawn,itype)' );
2638     ( $idr_mayrenew, $idr_error ) =
2639     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
2640     is( $idr_mayrenew, 1, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
2641     is( $idr_error, undef, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
2642
2643     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
2644
2645     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2646     ( $idr_mayrenew, $idr_error ) =
2647     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2648     is( $idr_mayrenew, 0, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
2649     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 3 rules matched (withdrawn,itype, location)' );
2650     ( $idr_mayrenew, $idr_error ) =
2651     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
2652     is( $idr_mayrenew, 1, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
2653     is( $idr_error, undef, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
2654
2655     $idr_rules="itemcallnumber: [NULL]";
2656     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2657     ( $idr_mayrenew, $idr_error ) =
2658     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2659     is( $idr_mayrenew, 0, 'Renewal blocked for undef when NULL in pref' );
2660     $idr_rules="itemcallnumber: ['']";
2661     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2662     ( $idr_mayrenew, $idr_error ) =
2663     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2664     is( $idr_mayrenew, 1, 'Renewal not blocked for undef when "" in pref' );
2665
2666     $idr_rules="itemnotes: [NULL]";
2667     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2668     ( $idr_mayrenew, $idr_error ) =
2669     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2670     is( $idr_mayrenew, 1, 'Renewal not blocked for "" when NULL in pref' );
2671     $idr_rules="itemnotes: ['']";
2672     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2673     ( $idr_mayrenew, $idr_error ) =
2674     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2675     is( $idr_mayrenew, 0, 'Renewal blocked for empty string when "" in pref' );
2676 };
2677
2678 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
2679     plan tests => 2;
2680
2681     t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
2682     my $library = $builder->build( { source => 'Branch' } );
2683     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
2684
2685     my $itemtype = $builder->build(
2686         {
2687             source => 'Itemtype',
2688             value  => { notforloan => undef, }
2689         }
2690     );
2691
2692     my $biblioitem = $builder->build( { source => 'Biblioitem', value => { itemtype => $itemtype->{itemtype} } } );
2693     my $item = $builder->build_object(
2694         {
2695             class => 'Koha::Items',
2696             value  => {
2697                 homebranch    => $library->{branchcode},
2698                 holdingbranch => $library->{branchcode},
2699                 notforloan    => 0,
2700                 itemlost      => 0,
2701                 withdrawn     => 0,
2702                 biblionumber  => $biblioitem->{biblionumber},
2703                 biblioitemnumber => $biblioitem->{biblioitemnumber},
2704             }
2705         }
2706     )->store;
2707
2708     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2709     is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
2710     is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
2711 };
2712
2713 subtest 'CanBookBeIssued | notforloan' => sub {
2714     plan tests => 2;
2715
2716     t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
2717
2718     my $library = $builder->build( { source => 'Branch' } );
2719     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
2720
2721     my $itemtype = $builder->build(
2722         {
2723             source => 'Itemtype',
2724             value  => { notforloan => undef, }
2725         }
2726     );
2727
2728     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
2729     my $item = $builder->build_object(
2730         {
2731             class => 'Koha::Items',
2732             value  => {
2733                 homebranch    => $library->{branchcode},
2734                 holdingbranch => $library->{branchcode},
2735                 notforloan    => 0,
2736                 itemlost      => 0,
2737                 withdrawn     => 0,
2738                 itype         => $itemtype->{itemtype},
2739                 biblionumber  => $biblioitem->{biblionumber},
2740                 biblioitemnumber => $biblioitem->{biblioitemnumber},
2741             }
2742         }
2743     )->store;
2744
2745     my ( $issuingimpossible, $needsconfirmation );
2746
2747
2748     subtest 'item-level_itypes = 1' => sub {
2749         plan tests => 6;
2750
2751         t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
2752         # Is for loan at item type and item level
2753         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2754         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
2755         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
2756
2757         # not for loan at item type level
2758         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
2759         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2760         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2761         is_deeply(
2762             $issuingimpossible,
2763             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
2764             'Item can not be issued, not for loan at item type level'
2765         );
2766
2767         # not for loan at item level
2768         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
2769         $item->notforloan( 1 )->store;
2770         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2771         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2772         is_deeply(
2773             $issuingimpossible,
2774             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
2775             'Item can not be issued, not for loan at item type level'
2776         );
2777     };
2778
2779     subtest 'item-level_itypes = 0' => sub {
2780         plan tests => 6;
2781
2782         t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
2783
2784         # We set another itemtype for biblioitem
2785         my $itemtype = $builder->build(
2786             {
2787                 source => 'Itemtype',
2788                 value  => { notforloan => undef, }
2789             }
2790         );
2791
2792         # for loan at item type and item level
2793         $item->notforloan(0)->store;
2794         $item->biblioitem->itemtype($itemtype->{itemtype})->store;
2795         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2796         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
2797         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
2798
2799         # not for loan at item type level
2800         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
2801         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2802         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2803         is_deeply(
2804             $issuingimpossible,
2805             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
2806             'Item can not be issued, not for loan at item type level'
2807         );
2808
2809         # not for loan at item level
2810         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
2811         $item->notforloan( 1 )->store;
2812         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2813         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2814         is_deeply(
2815             $issuingimpossible,
2816             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
2817             'Item can not be issued, not for loan at item type level'
2818         );
2819     };
2820
2821     # TODO test with AllowNotForLoanOverride = 1
2822 };
2823
2824 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
2825     plan tests => 1;
2826
2827     t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
2828     my $item = $builder->build_object({ class => 'Koha::Items', value  => { onloan => '2018-01-01' }});
2829     AddReturn( $item->barcode, $item->homebranch );
2830     $item->discard_changes; # refresh
2831     is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
2832 };
2833
2834 $schema->storage->txn_rollback;
2835 C4::Context->clear_syspref_cache();
2836 $cache->clear_from_cache('single_holidays');
2837
2838 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
2839
2840     plan tests => 13;
2841
2842     $schema->storage->txn_begin;
2843
2844     my $issuing_charges = 15;
2845     my $title   = 'A title';
2846     my $author  = 'Author, An';
2847     my $barcode = 'WHATARETHEODDS';
2848
2849     my $circ = Test::MockModule->new('C4::Circulation');
2850     $circ->mock(
2851         'GetIssuingCharges',
2852         sub {
2853             return $issuing_charges;
2854         }
2855     );
2856
2857     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
2858     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
2859     my $patron   = $builder->build_object({
2860         class => 'Koha::Patrons',
2861         value => { branchcode => $library->id }
2862     });
2863
2864     my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
2865     my ( undef, undef, $item_id ) = AddItem(
2866         {
2867             homebranch       => $library->id,
2868             holdingbranch    => $library->id,
2869             barcode          => $barcode,
2870             replacementprice => 23.00,
2871             itype            => $itemtype->id
2872         },
2873         $biblio->biblionumber
2874     );
2875     my $item = Koha::Items->find( $item_id );
2876
2877     my $context = Test::MockModule->new('C4::Context');
2878     $context->mock( userenv => { branch => $library->id } );
2879
2880     # Check the item out
2881     AddIssue( $patron->unblessed, $item->barcode );
2882
2883     t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
2884     my $date = output_pref( { dt => dt_from_string(), datenonly => 1, dateformat => 'iso' } );
2885     my $old_log_size = scalar( @{ GetLogs( $date, $date, undef, ["CIRCULATION"], ["RENEWAL"] ) } );
2886     AddRenewal( $patron->id, $item->id, $library->id );
2887     my $new_log_size = scalar( @{ GetLogs( $date, $date, undef, ["CIRCULATION"], ["RENEWAL"] ) } );
2888     is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
2889
2890     my $checkouts = $patron->checkouts;
2891     # The following will fail if run on 00:00:00
2892     unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
2893
2894     t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
2895     $date = output_pref( { dt => dt_from_string(), datenonly => 1, dateformat => 'iso' } );
2896     $old_log_size = scalar( @{ GetLogs( $date, $date, undef, ["CIRCULATION"], ["RENEWAL"] ) } );
2897     AddRenewal( $patron->id, $item->id, $library->id );
2898     $new_log_size = scalar( @{ GetLogs( $date, $date, undef, ["CIRCULATION"], ["RENEWAL"] ) } );
2899     is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
2900
2901     my $lines = Koha::Account::Lines->search({
2902         borrowernumber => $patron->id,
2903         itemnumber     => $item->id
2904     });
2905
2906     is( $lines->count, 3 );
2907
2908     my $line = $lines->next;
2909     is( $line->accounttype, 'Rent',       'The issuing charge generates an accountline' );
2910     is( $line->branchcode,  $library->id, 'AddIssuingCharge correctly sets branchcode' );
2911     is( $line->description, 'Rental',     'AddIssuingCharge set a hardcoded description for the accountline' );
2912
2913     $line = $lines->next;
2914     is( $line->accounttype, 'Rent', 'Fine on renewed item is closed out properly' );
2915     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
2916     is( $line->description, "Renewal of Rental Item $title $barcode", 'AddRenewal set a hardcoded description for the accountline' );
2917
2918     $line = $lines->next;
2919     is( $line->accounttype, 'Rent', 'Fine on renewed item is closed out properly' );
2920     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
2921     is( $line->description, "Renewal of Rental Item $title $barcode", 'AddRenewal set a hardcoded description for the accountline' );
2922
2923     $schema->storage->txn_rollback;
2924 };
2925
2926 subtest 'ProcessOfflinePayment() tests' => sub {
2927
2928     plan tests => 4;
2929
2930     $schema->storage->txn_begin;
2931
2932     my $amount = 123;
2933
2934     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
2935     my $library = $builder->build_object({ class => 'Koha::Libraries' });
2936     my $result  = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
2937
2938     is( $result, 'Success.', 'The right string is returned' );
2939
2940     my $lines = $patron->account->lines;
2941     is( $lines->count, 1, 'line created correctly');
2942
2943     my $line = $lines->next;
2944     is( $line->amount+0, $amount * -1, 'amount picked from params' );
2945     is( $line->branchcode, $library->id, 'branchcode set correctly' );
2946
2947     $schema->storage->txn_rollback;
2948 };
2949
2950
2951
2952 sub set_userenv {
2953     my ( $library ) = @_;
2954     t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
2955 }
2956
2957 sub str {
2958     my ( $error, $question, $alert ) = @_;
2959     my $s;
2960     $s  = %$error    ? ' (error: '    . join( ' ', keys %$error    ) . ')' : '';
2961     $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
2962     $s .= %$alert    ? ' (alert: '    . join( ' ', keys %$alert    ) . ')' : '';
2963     return $s;
2964 }
2965
2966 sub test_debarment_on_checkout {
2967     my ($params) = @_;
2968     my $item     = $params->{item};
2969     my $library  = $params->{library};
2970     my $patron   = $params->{patron};
2971     my $due_date = $params->{due_date} || dt_from_string;
2972     my $return_date = $params->{return_date} || dt_from_string;
2973     my $expected_expiration_date = $params->{expiration_date};
2974
2975     $expected_expiration_date = output_pref(
2976         {
2977             dt         => $expected_expiration_date,
2978             dateformat => 'sql',
2979             dateonly   => 1,
2980         }
2981     );
2982     my @caller      = caller;
2983     my $line_number = $caller[2];
2984     AddIssue( $patron, $item->{barcode}, $due_date );
2985
2986     my ( undef, $message ) = AddReturn( $item->{barcode}, $library->{branchcode},
2987         undef, undef, $return_date );
2988     is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
2989         or diag('AddReturn returned message ' . Dumper $message );
2990     my $debarments = Koha::Patron::Debarments::GetDebarments(
2991         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2992     is( scalar(@$debarments), 1, 'Test at line ' . $line_number );
2993
2994     is( $debarments->[0]->{expiration},
2995         $expected_expiration_date, 'Test at line ' . $line_number );
2996     Koha::Patron::Debarments::DelUniqueDebarment(
2997         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
2998 }