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