436578450eacf0f7f5f6c1c9f7ef49876ab923c7
[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     is( $fines->next->accounttype, 'F', 'Fine on renewed item is closed out properly' );
513     is( $fines->next->accounttype, 'F', '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        => 'fine',
696                 item_id     => $item_to_auto_renew->{itemnumber},
697                 description => "Some fines"
698             }
699         )->accounttype('F')->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        => 'fine',
710                 item_id     => $item_to_auto_renew->{itemnumber},
711                 description => "Some fines"
712             }
713         )->accounttype('F')->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        => 'fine',
724                 item_id     => $item_to_auto_renew->{itemnumber},
725                 description => "Some fines"
726             }
727         )->accounttype('F')->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, 'FU', 'Account line type is FU' );
862     is( $line->amountoutstanding, '15.000000', 'Account line amount outstanding is 15.00' );
863     is( $line->amount, '15.000000', 'Account line amount is 15.00' );
864     is( $line->issue_id, $issue->id, 'Account line issue id matches' );
865
866     my $offset = Koha::Account::Offsets->search({ debit_id => $line->id })->next();
867     is( $offset->type, 'Fine', 'Account offset type is Fine' );
868     is( $offset->amount, '15.000000', 'Account offset amount is 15.00' );
869
870     t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
871     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
872
873     LostItem( $item_1->itemnumber, 'test', 1 );
874
875     $line = Koha::Account::Lines->find($line->id);
876     is( $line->accounttype, 'F', 'Account type correctly changed from FU to F' );
877
878     my $item = Koha::Items->find($item_1->itemnumber);
879     ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
880     my $checkout = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber });
881     is( $checkout, undef, 'LostItem called with forced return has checked in the item' );
882
883     my $total_due = $dbh->selectrow_array(
884         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
885         undef, $renewing_borrower->{borrowernumber}
886     );
887
888     is( $total_due, '15.000000', 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
889
890     C4::Context->dbh->do("DELETE FROM accountlines");
891
892     C4::Overdues::UpdateFine(
893         {
894             issue_id       => $issue2->id(),
895             itemnumber     => $item_2->itemnumber,
896             borrowernumber => $renewing_borrower->{borrowernumber},
897             amount         => 15.00,
898             type           => q{},
899             due            => Koha::DateUtils::output_pref($datedue)
900         }
901     );
902
903     LostItem( $item_2->itemnumber, 'test', 0 );
904
905     my $item2 = Koha::Items->find($item_2->itemnumber);
906     ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
907     ok( Koha::Checkouts->find({ itemnumber => $item_2->itemnumber }), 'LostItem called without forced return has checked in the item' );
908
909     $total_due = $dbh->selectrow_array(
910         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
911         undef, $renewing_borrower->{borrowernumber}
912     );
913
914     ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
915
916     my $future = dt_from_string();
917     $future->add( days => 7 );
918     my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
919     ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
920
921     # Users cannot renew any item if there is an overdue item
922     t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
923     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_6->itemnumber);
924     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
925     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_7->itemnumber);
926     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
927
928     my $manager = $builder->build_object({ class => "Koha::Patrons" });
929     t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
930     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
931     $checkout = Koha::Checkouts->find( { itemnumber => $item_3->itemnumber } );
932     LostItem( $item_3->itemnumber, 'test', 0 );
933     my $accountline = Koha::Account::Lines->find( { itemnumber => $item_3->itemnumber } );
934     is( $accountline->issue_id, $checkout->id, "Issue id added for lost replacement fee charge" );
935     is(
936         $accountline->description,
937         sprintf( "%s %s %s",
938             $item_3->biblio->title  || '',
939             $item_3->barcode        || '',
940             $item_3->itemcallnumber || '' ),
941         "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
942     );
943   }
944
945 {
946     # GetUpcomingDueIssues tests
947     my $branch   = $library2->{branchcode};
948
949     #Create another record
950     my $biblio2 = $builder->build_sample_biblio();
951
952     #Create third item
953     my $item_1 = Koha::Items->find($reused_itemnumber_1);
954     my $item_2 = Koha::Items->find($reused_itemnumber_2);
955     my $item_3 = $builder->build_sample_item(
956         {
957             biblionumber     => $biblio2->biblionumber,
958             library          => $branch,
959             itype            => $itemtype,
960         }
961     );
962
963
964     # Create a borrower
965     my %a_borrower_data = (
966         firstname =>  'Fridolyn',
967         surname => 'SOMERS',
968         categorycode => $patron_category->{categorycode},
969         branchcode => $branch,
970     );
971
972     my $a_borrower_borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
973     my $a_borrower = Koha::Patrons->find( $a_borrower_borrowernumber )->unblessed;
974
975     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
976     my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
977     my $today = DateTime->today(time_zone => C4::Context->tz());
978
979     my $issue = AddIssue( $a_borrower, $item_1->barcode, $yesterday );
980     my $datedue = dt_from_string( $issue->date_due() );
981     my $issue2 = AddIssue( $a_borrower, $item_2->barcode, $two_days_ahead );
982     my $datedue2 = dt_from_string( $issue->date_due() );
983
984     my $upcoming_dues;
985
986     # GetUpcomingDueIssues tests
987     for my $i(0..1) {
988         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
989         is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
990     }
991
992     #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
993     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
994     is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
995
996     for my $i(3..5) {
997         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
998         is ( scalar( @$upcoming_dues ), 1,
999             "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
1000     }
1001
1002     # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
1003
1004     my $issue3 = AddIssue( $a_borrower, $item_3->barcode, $today );
1005
1006     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
1007     is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
1008
1009     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
1010     is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
1011
1012     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
1013     is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
1014
1015     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 }  );
1016     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1017
1018     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
1019     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
1020
1021     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
1022     is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
1023
1024 }
1025
1026 {
1027     my $branch   = $library2->{branchcode};
1028
1029     my $biblio = $builder->build_sample_biblio();
1030
1031     #Create third item
1032     my $item = $builder->build_sample_item(
1033         {
1034             biblionumber     => $biblio->biblionumber,
1035             library          => $branch,
1036             itype            => $itemtype,
1037         }
1038     );
1039
1040     # Create a borrower
1041     my %a_borrower_data = (
1042         firstname =>  'Kyle',
1043         surname => 'Hall',
1044         categorycode => $patron_category->{categorycode},
1045         branchcode => $branch,
1046     );
1047
1048     my $borrowernumber = Koha::Patron->new(\%a_borrower_data)->store->borrowernumber;
1049
1050     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1051     my $issue = AddIssue( $borrower, $item->barcode );
1052     UpdateFine(
1053         {
1054             issue_id       => $issue->id(),
1055             itemnumber     => $item->itemnumber,
1056             borrowernumber => $borrowernumber,
1057             amount         => 0,
1058             type           => q{}
1059         }
1060     );
1061
1062     my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $item->itemnumber );
1063     my $count = $hr->{count};
1064
1065     is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
1066 }
1067
1068 {
1069     $dbh->do('DELETE FROM issues');
1070     $dbh->do('DELETE FROM items');
1071     $dbh->do('DELETE FROM issuingrules');
1072     Koha::CirculationRules->search()->delete();
1073     $dbh->do(
1074         q{
1075         INSERT INTO issuingrules ( categorycode, branchcode, itemtype, reservesallowed, issuelength, lengthunit, renewalsallowed, renewalperiod,
1076                     norenewalbefore, auto_renew, fine, chargeperiod ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
1077         },
1078         {},
1079         '*', '*', '*', 25,
1080         14,  'days',
1081         1,   7,
1082         undef,  0,
1083         .10, 1
1084     );
1085     Koha::CirculationRules->set_rules(
1086         {
1087             categorycode => '*',
1088             itemtype     => '*',
1089             branchcode   => '*',
1090             rules        => {
1091                 maxissueqty => 20
1092             }
1093         }
1094     );
1095     my $biblio = $builder->build_sample_biblio();
1096
1097     my $item_1 = $builder->build_sample_item(
1098         {
1099             biblionumber     => $biblio->biblionumber,
1100             library          => $library2->{branchcode},
1101             itype            => $itemtype,
1102         }
1103     );
1104
1105     my $item_2= $builder->build_sample_item(
1106         {
1107             biblionumber     => $biblio->biblionumber,
1108             library          => $library2->{branchcode},
1109             itype            => $itemtype,
1110         }
1111     );
1112
1113     my $borrowernumber1 = Koha::Patron->new({
1114         firstname    => 'Kyle',
1115         surname      => 'Hall',
1116         categorycode => $patron_category->{categorycode},
1117         branchcode   => $library2->{branchcode},
1118     })->store->borrowernumber;
1119     my $borrowernumber2 = Koha::Patron->new({
1120         firstname    => 'Chelsea',
1121         surname      => 'Hall',
1122         categorycode => $patron_category->{categorycode},
1123         branchcode   => $library2->{branchcode},
1124     })->store->borrowernumber;
1125
1126     my $borrower1 = Koha::Patrons->find( $borrowernumber1 )->unblessed;
1127     my $borrower2 = Koha::Patrons->find( $borrowernumber2 )->unblessed;
1128
1129     my $issue = AddIssue( $borrower1, $item_1->barcode );
1130
1131     my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1132     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
1133
1134     AddReserve(
1135         $library2->{branchcode}, $borrowernumber2, $biblio->biblionumber,
1136         '',  1, undef, undef, '',
1137         undef, undef, undef
1138     );
1139
1140     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 0");
1141     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1142     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1143     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
1144
1145     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 0");
1146     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1147     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1148     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
1149
1150     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
1151     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
1152     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1153     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
1154
1155     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
1156     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
1157     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1158     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
1159
1160     # Setting item not checked out to be not for loan but holdable
1161     ModItem({ notforloan => -1 }, $biblio->biblionumber, $item_2->itemnumber);
1162
1163     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $item_1->itemnumber );
1164     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' );
1165 }
1166
1167 {
1168     # Don't allow renewing onsite checkout
1169     my $branch   = $library->{branchcode};
1170
1171     #Create another record
1172     my $biblio = $builder->build_sample_biblio();
1173
1174     my $item = $builder->build_sample_item(
1175         {
1176             biblionumber     => $biblio->biblionumber,
1177             library          => $branch,
1178             itype            => $itemtype,
1179         }
1180     );
1181
1182     my $borrowernumber = Koha::Patron->new({
1183         firstname =>  'fn',
1184         surname => 'dn',
1185         categorycode => $patron_category->{categorycode},
1186         branchcode => $branch,
1187     })->store->borrowernumber;
1188
1189     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
1190
1191     my $issue = AddIssue( $borrower, $item->barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
1192     my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $item->itemnumber );
1193     is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
1194     is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
1195 }
1196
1197 {
1198     my $library = $builder->build({ source => 'Branch' });
1199
1200     my $biblio = $builder->build_sample_biblio();
1201
1202     my $item = $builder->build_sample_item(
1203         {
1204             biblionumber     => $biblio->biblionumber,
1205             library          => $library->{branchcode},
1206             itype            => $itemtype,
1207         }
1208     );
1209
1210     my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode}, categorycode => $patron_category->{categorycode} } } );
1211
1212     my $issue = AddIssue( $patron, $item->barcode );
1213     UpdateFine(
1214         {
1215             issue_id       => $issue->id(),
1216             itemnumber     => $item->itemnumber,
1217             borrowernumber => $patron->{borrowernumber},
1218             amount         => 1,
1219             type           => q{}
1220         }
1221     );
1222     UpdateFine(
1223         {
1224             issue_id       => $issue->id(),
1225             itemnumber     => $item->itemnumber,
1226             borrowernumber => $patron->{borrowernumber},
1227             amount         => 2,
1228             type           => q{}
1229         }
1230     );
1231     is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1232 }
1233
1234 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1235     plan tests => 24;
1236
1237     my $homebranch    = $builder->build( { source => 'Branch' } );
1238     my $holdingbranch = $builder->build( { source => 'Branch' } );
1239     my $otherbranch   = $builder->build( { source => 'Branch' } );
1240     my $patron_1      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1241     my $patron_2      = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1242
1243     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1244     my $item = $builder->build(
1245         {   source => 'Item',
1246             value  => {
1247                 homebranch    => $homebranch->{branchcode},
1248                 holdingbranch => $holdingbranch->{branchcode},
1249                 biblionumber  => $biblioitem->{biblionumber}
1250             }
1251         }
1252     );
1253
1254     set_userenv($holdingbranch);
1255
1256     my $issue = AddIssue( $patron_1->unblessed, $item->{barcode} );
1257     is( ref($issue), 'Koha::Checkout', 'AddIssue should return a Koha::Checkout object' );
1258
1259     my ( $error, $question, $alerts );
1260
1261     # AllowReturnToBranch == anywhere
1262     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1263     ## Test that unknown barcodes don't generate internal server errors
1264     set_userenv($homebranch);
1265     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, 'KohaIsAwesome' );
1266     ok( $error->{UNKNOWN_BARCODE}, '"KohaIsAwesome" is not a valid barcode as expected.' );
1267     ## Can be issued from homebranch
1268     set_userenv($homebranch);
1269     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1270     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1271     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1272     ## Can be issued from holdingbranch
1273     set_userenv($holdingbranch);
1274     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1275     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1276     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1277     ## Can be issued from another branch
1278     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1279     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1280     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1281
1282     # AllowReturnToBranch == holdingbranch
1283     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1284     ## Cannot be issued from homebranch
1285     set_userenv($homebranch);
1286     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1287     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1288     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1289     is( $error->{branch_to_return},         $holdingbranch->{branchcode} );
1290     ## Can be issued from holdinbranch
1291     set_userenv($holdingbranch);
1292     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1293     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1294     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1295     ## Cannot be issued from another branch
1296     set_userenv($otherbranch);
1297     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1298     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1299     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1300     is( $error->{branch_to_return},         $holdingbranch->{branchcode} );
1301
1302     # AllowReturnToBranch == homebranch
1303     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1304     ## Can be issued from holdinbranch
1305     set_userenv($homebranch);
1306     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1307     is( keys(%$error) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1308     is( exists $question->{ISSUED_TO_ANOTHER}, 1, 'ISSUED_TO_ANOTHER must be set' );
1309     ## Cannot be issued from holdinbranch
1310     set_userenv($holdingbranch);
1311     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1312     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1313     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1314     is( $error->{branch_to_return},         $homebranch->{branchcode} );
1315     ## Cannot be issued from holdinbranch
1316     set_userenv($otherbranch);
1317     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1318     is( keys(%$question) + keys(%$alerts), 0, 'There should not be any errors or alerts (impossible)' . str($error, $question, $alerts) );
1319     is( exists $error->{RETURN_IMPOSSIBLE}, 1, 'RETURN_IMPOSSIBLE must be set' );
1320     is( $error->{branch_to_return},         $homebranch->{branchcode} );
1321
1322     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1323 };
1324
1325 subtest 'AddIssue & AllowReturnToBranch' => sub {
1326     plan tests => 9;
1327
1328     my $homebranch    = $builder->build( { source => 'Branch' } );
1329     my $holdingbranch = $builder->build( { source => 'Branch' } );
1330     my $otherbranch   = $builder->build( { source => 'Branch' } );
1331     my $patron_1      = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1332     my $patron_2      = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1333
1334     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1335     my $item = $builder->build(
1336         {   source => 'Item',
1337             value  => {
1338                 homebranch    => $homebranch->{branchcode},
1339                 holdingbranch => $holdingbranch->{branchcode},
1340                 notforloan    => 0,
1341                 itemlost      => 0,
1342                 withdrawn     => 0,
1343                 biblionumber  => $biblioitem->{biblionumber}
1344             }
1345         }
1346     );
1347
1348     set_userenv($holdingbranch);
1349
1350     my $ref_issue = 'Koha::Checkout';
1351     my $issue = AddIssue( $patron_1, $item->{barcode} );
1352
1353     my ( $error, $question, $alerts );
1354
1355     # AllowReturnToBranch == homebranch
1356     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1357     ## Can be issued from homebranch
1358     set_userenv($homebranch);
1359     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1360     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1361     ## Can be issued from holdinbranch
1362     set_userenv($holdingbranch);
1363     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1364     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1365     ## Can be issued from another branch
1366     set_userenv($otherbranch);
1367     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1368     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1369
1370     # AllowReturnToBranch == holdinbranch
1371     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1372     ## Cannot be issued from homebranch
1373     set_userenv($homebranch);
1374     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1375     ## Can be issued from holdingbranch
1376     set_userenv($holdingbranch);
1377     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1378     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1379     ## Cannot be issued from another branch
1380     set_userenv($otherbranch);
1381     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1382
1383     # AllowReturnToBranch == homebranch
1384     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1385     ## Can be issued from homebranch
1386     set_userenv($homebranch);
1387     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1388     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1389     ## Cannot be issued from holdinbranch
1390     set_userenv($holdingbranch);
1391     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1392     ## Cannot be issued from another branch
1393     set_userenv($otherbranch);
1394     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1395     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1396 };
1397
1398 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1399     plan tests => 8;
1400
1401     my $library = $builder->build( { source => 'Branch' } );
1402     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1403
1404     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1405     my $item_1 = $builder->build(
1406         {   source => 'Item',
1407             value  => {
1408                 homebranch    => $library->{branchcode},
1409                 holdingbranch => $library->{branchcode},
1410                 biblionumber  => $biblioitem_1->{biblionumber}
1411             }
1412         }
1413     );
1414     my $biblioitem_2 = $builder->build( { source => 'Biblioitem' } );
1415     my $item_2 = $builder->build(
1416         {   source => 'Item',
1417             value  => {
1418                 homebranch    => $library->{branchcode},
1419                 holdingbranch => $library->{branchcode},
1420                 biblionumber  => $biblioitem_2->{biblionumber}
1421             }
1422         }
1423     );
1424
1425     my ( $error, $question, $alerts );
1426
1427     # Patron cannot issue item_1, they have overdues
1428     my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1429     my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, $yesterday );    # Add an overdue
1430
1431     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1432     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1433     is( keys(%$error) + keys(%$alerts),  0, 'No key for error and alert' . str($error, $question, $alerts) );
1434     is( $question->{USERBLOCKEDOVERDUE}, 1, 'OverduesBlockCirc=confirmation, USERBLOCKEDOVERDUE should be set for question' );
1435
1436     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1437     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1438     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1439     is( $error->{USERBLOCKEDOVERDUE},      1, 'OverduesBlockCirc=block, USERBLOCKEDOVERDUE should be set for error' );
1440
1441     # Patron cannot issue item_1, they are debarred
1442     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1443     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber, expiration => $tomorrow } );
1444     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1445     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1446     is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ), 'USERBLOCKEDWITHENDDATE should be tomorrow' );
1447
1448     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->borrowernumber } );
1449     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1450     is( keys(%$question) + keys(%$alerts),  0, 'No key for question and alert ' . str($error, $question, $alerts) );
1451     is( $error->{USERBLOCKEDNOENDDATE},    '9999-12-31', 'USERBLOCKEDNOENDDATE should be 9999-12-31 for unlimited debarments' );
1452 };
1453
1454 subtest 'CanBookBeIssued + Statistic patrons "X"' => sub {
1455     plan tests => 1;
1456
1457     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1458     my $patron_category_x = $builder->build_object(
1459         {
1460             class => 'Koha::Patron::Categories',
1461             value => { category_type => 'X' }
1462         }
1463     );
1464     my $patron = $builder->build_object(
1465         {
1466             class => 'Koha::Patrons',
1467             value => {
1468                 categorycode  => $patron_category_x->categorycode,
1469                 gonenoaddress => undef,
1470                 lost          => undef,
1471                 debarred      => undef,
1472                 borrowernotes => ""
1473             }
1474         }
1475     );
1476     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1477     my $item_1 = $builder->build(
1478         {
1479             source => 'Item',
1480             value  => {
1481                 homebranch    => $library->branchcode,
1482                 holdingbranch => $library->branchcode,
1483                 biblionumber  => $biblioitem_1->{biblionumber}
1484             }
1485         }
1486     );
1487
1488     my ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_1->{barcode} );
1489     is( $error->{STATS}, 1, '"Error" flag "STATS" must be set if CanBookBeIssued is called with a statistic patron (category_type=X)' );
1490
1491     # TODO There are other tests to provide here
1492 };
1493
1494 subtest 'MultipleReserves' => sub {
1495     plan tests => 3;
1496
1497     my $biblio = $builder->build_sample_biblio();
1498
1499     my $branch = $library2->{branchcode};
1500
1501     my $item_1 = $builder->build_sample_item(
1502         {
1503             biblionumber     => $biblio->biblionumber,
1504             library          => $branch,
1505             replacementprice => 12.00,
1506             itype            => $itemtype,
1507         }
1508     );
1509
1510     my $item_2 = $builder->build_sample_item(
1511         {
1512             biblionumber     => $biblio->biblionumber,
1513             library          => $branch,
1514             replacementprice => 12.00,
1515             itype            => $itemtype,
1516         }
1517     );
1518
1519     my $bibitems       = '';
1520     my $priority       = '1';
1521     my $resdate        = undef;
1522     my $expdate        = undef;
1523     my $notes          = '';
1524     my $checkitem      = undef;
1525     my $found          = undef;
1526
1527     my %renewing_borrower_data = (
1528         firstname =>  'John',
1529         surname => 'Renewal',
1530         categorycode => $patron_category->{categorycode},
1531         branchcode => $branch,
1532     );
1533     my $renewing_borrowernumber = Koha::Patron->new(\%renewing_borrower_data)->store->borrowernumber;
1534     my $renewing_borrower = Koha::Patrons->find( $renewing_borrowernumber )->unblessed;
1535     my $issue = AddIssue( $renewing_borrower, $item_1->barcode);
1536     my $datedue = dt_from_string( $issue->date_due() );
1537     is (defined $issue->date_due(), 1, "item 1 checked out");
1538     my $borrowing_borrowernumber = Koha::Checkouts->find({ itemnumber => $item_1->itemnumber })->borrowernumber;
1539
1540     my %reserving_borrower_data1 = (
1541         firstname =>  'Katrin',
1542         surname => 'Reservation',
1543         categorycode => $patron_category->{categorycode},
1544         branchcode => $branch,
1545     );
1546     my $reserving_borrowernumber1 = Koha::Patron->new(\%reserving_borrower_data1)->store->borrowernumber;
1547     AddReserve(
1548         $branch, $reserving_borrowernumber1, $biblio->biblionumber,
1549         $bibitems,  $priority, $resdate, $expdate, $notes,
1550         'a title', $checkitem, $found
1551     );
1552
1553     my %reserving_borrower_data2 = (
1554         firstname =>  'Kirk',
1555         surname => 'Reservation',
1556         categorycode => $patron_category->{categorycode},
1557         branchcode => $branch,
1558     );
1559     my $reserving_borrowernumber2 = Koha::Patron->new(\%reserving_borrower_data2)->store->borrowernumber;
1560     AddReserve(
1561         $branch, $reserving_borrowernumber2, $biblio->biblionumber,
1562         $bibitems,  $priority, $resdate, $expdate, $notes,
1563         'a title', $checkitem, $found
1564     );
1565
1566     {
1567         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1568         is($renewokay, 0, 'Bug 17941 - should cover the case where 2 books are both reserved, so failing');
1569     }
1570
1571     my $item_3 = $builder->build_sample_item(
1572         {
1573             biblionumber     => $biblio->biblionumber,
1574             library          => $branch,
1575             replacementprice => 12.00,
1576             itype            => $itemtype,
1577         }
1578     );
1579
1580     {
1581         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1);
1582         is($renewokay, 1, 'Bug 17941 - should cover the case where 2 books are reserved, but a third one is available');
1583     }
1584 };
1585
1586 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
1587     plan tests => 5;
1588
1589     my $library = $builder->build( { source => 'Branch' } );
1590     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
1591
1592     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1593     my $biblionumber = $biblioitem->{biblionumber};
1594     my $item_1 = $builder->build(
1595         {   source => 'Item',
1596             value  => {
1597                 homebranch    => $library->{branchcode},
1598                 holdingbranch => $library->{branchcode},
1599                 biblionumber  => $biblionumber,
1600             }
1601         }
1602     );
1603     my $item_2 = $builder->build(
1604         {   source => 'Item',
1605             value  => {
1606                 homebranch    => $library->{branchcode},
1607                 holdingbranch => $library->{branchcode},
1608                 biblionumber  => $biblionumber,
1609             }
1610         }
1611     );
1612
1613     my ( $error, $question, $alerts );
1614     my $issue = AddIssue( $patron->unblessed, $item_1->{barcode}, dt_from_string->add( days => 1 ) );
1615
1616     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1617     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1618     is( keys(%$error) + keys(%$alerts),  0, 'No error or alert should be raised' . str($error, $question, $alerts) );
1619     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) );
1620
1621     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1622     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1623     is( keys(%$error) + keys(%$question) + keys(%$alerts),  0, 'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1' . str($error, $question, $alerts) );
1624
1625     # Add a subscription
1626     Koha::Subscription->new({ biblionumber => $biblionumber })->store;
1627
1628     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1629     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1630     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) );
1631
1632     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1633     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1634     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) );
1635 };
1636
1637 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
1638     plan tests => 8;
1639
1640     my $library = $builder->build( { source => 'Branch' } );
1641     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1642
1643     # Add 2 items
1644     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1645     my $item_1 = $builder->build(
1646         {
1647             source => 'Item',
1648             value  => {
1649                 homebranch    => $library->{branchcode},
1650                 holdingbranch => $library->{branchcode},
1651                 notforloan    => 0,
1652                 itemlost      => 0,
1653                 withdrawn     => 0,
1654                 biblionumber  => $biblioitem_1->{biblionumber}
1655             }
1656         }
1657     );
1658     my $biblioitem_2 = $builder->build( { source => 'Biblioitem' } );
1659     my $item_2 = $builder->build(
1660         {
1661             source => 'Item',
1662             value  => {
1663                 homebranch    => $library->{branchcode},
1664                 holdingbranch => $library->{branchcode},
1665                 notforloan    => 0,
1666                 itemlost      => 0,
1667                 withdrawn     => 0,
1668                 biblionumber  => $biblioitem_2->{biblionumber}
1669             }
1670         }
1671     );
1672
1673     # And the issuing rule
1674     Koha::IssuingRules->search->delete;
1675     my $rule = Koha::IssuingRule->new(
1676         {
1677             categorycode => '*',
1678             itemtype     => '*',
1679             branchcode   => '*',
1680             issuelength  => 1,
1681             firstremind  => 1,        # 1 day of grace
1682             finedays     => 2,        # 2 days of fine per day of overdue
1683             lengthunit   => 'days',
1684         }
1685     );
1686     $rule->store();
1687
1688     # Patron cannot issue item_1, they have overdues
1689     my $five_days_ago = dt_from_string->subtract( days => 5 );
1690     my $ten_days_ago  = dt_from_string->subtract( days => 10 );
1691     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
1692     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
1693       ;    # Add another overdue
1694
1695     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
1696     AddReturn( $item_1->{barcode}, $library->{branchcode}, undef, dt_from_string );
1697     my $debarments = Koha::Patron::Debarments::GetDebarments(
1698         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1699     is( scalar(@$debarments), 1 );
1700
1701     # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
1702     # Same for the others
1703     my $expected_expiration = output_pref(
1704         {
1705             dt         => dt_from_string->add( days => ( 5 - 1 ) * 2 ),
1706             dateformat => 'sql',
1707             dateonly   => 1
1708         }
1709     );
1710     is( $debarments->[0]->{expiration}, $expected_expiration );
1711
1712     AddReturn( $item_2->{barcode}, $library->{branchcode}, undef, dt_from_string );
1713     $debarments = Koha::Patron::Debarments::GetDebarments(
1714         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1715     is( scalar(@$debarments), 1 );
1716     $expected_expiration = output_pref(
1717         {
1718             dt         => dt_from_string->add( days => ( 10 - 1 ) * 2 ),
1719             dateformat => 'sql',
1720             dateonly   => 1
1721         }
1722     );
1723     is( $debarments->[0]->{expiration}, $expected_expiration );
1724
1725     Koha::Patron::Debarments::DelUniqueDebarment(
1726         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1727
1728     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
1729     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
1730     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
1731       ;    # Add another overdue
1732     AddReturn( $item_1->{barcode}, $library->{branchcode}, undef, dt_from_string );
1733     $debarments = Koha::Patron::Debarments::GetDebarments(
1734         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1735     is( scalar(@$debarments), 1 );
1736     $expected_expiration = output_pref(
1737         {
1738             dt         => dt_from_string->add( days => ( 5 - 1 ) * 2 ),
1739             dateformat => 'sql',
1740             dateonly   => 1
1741         }
1742     );
1743     is( $debarments->[0]->{expiration}, $expected_expiration );
1744
1745     AddReturn( $item_2->{barcode}, $library->{branchcode}, undef, dt_from_string );
1746     $debarments = Koha::Patron::Debarments::GetDebarments(
1747         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1748     is( scalar(@$debarments), 1 );
1749     $expected_expiration = output_pref(
1750         {
1751             dt => dt_from_string->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
1752             dateformat => 'sql',
1753             dateonly   => 1
1754         }
1755     );
1756     is( $debarments->[0]->{expiration}, $expected_expiration );
1757 };
1758
1759 subtest 'AddReturn + suspension_chargeperiod' => sub {
1760     plan tests => 21;
1761
1762     my $library = $builder->build( { source => 'Branch' } );
1763     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1764
1765     # Add 2 items
1766     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1767     my $item_1 = $builder->build(
1768         {
1769             source => 'Item',
1770             value  => {
1771                 homebranch    => $library->{branchcode},
1772                 holdingbranch => $library->{branchcode},
1773                 notforloan    => 0,
1774                 itemlost      => 0,
1775                 withdrawn     => 0,
1776                 biblionumber  => $biblioitem_1->{biblionumber}
1777             }
1778         }
1779     );
1780
1781     # And the issuing rule
1782     Koha::IssuingRules->search->delete;
1783     my $rule = Koha::IssuingRule->new(
1784         {
1785             categorycode => '*',
1786             itemtype     => '*',
1787             branchcode   => '*',
1788             issuelength  => 1,
1789             firstremind  => 0,        # 0 day of grace
1790             finedays     => 2,        # 2 days of fine per day of overdue
1791             suspension_chargeperiod => 1,
1792             lengthunit   => 'days',
1793         }
1794     );
1795     $rule->store();
1796
1797     my $five_days_ago = dt_from_string->subtract( days => 5 );
1798     # We want to charge 2 days every day, without grace
1799     # With 5 days of overdue: 5 * Z
1800     my $expected_expiration = dt_from_string->add( days => ( 5 * 2 ) / 1 );
1801     test_debarment_on_checkout(
1802         {
1803             item            => $item_1,
1804             library         => $library,
1805             patron          => $patron,
1806             due_date        => $five_days_ago,
1807             expiration_date => $expected_expiration,
1808         }
1809     );
1810
1811     # We want to charge 2 days every 2 days, without grace
1812     # With 5 days of overdue: (5 * 2) / 2
1813     $rule->suspension_chargeperiod(2)->store;
1814     $expected_expiration = dt_from_string->add( days => floor( 5 * 2 ) / 2 );
1815     test_debarment_on_checkout(
1816         {
1817             item            => $item_1,
1818             library         => $library,
1819             patron          => $patron,
1820             due_date        => $five_days_ago,
1821             expiration_date => $expected_expiration,
1822         }
1823     );
1824
1825     # We want to charge 2 days every 3 days, with 1 day of grace
1826     # With 5 days of overdue: ((5-1) / 3 ) * 2
1827     $rule->suspension_chargeperiod(3)->store;
1828     $rule->firstremind(1)->store;
1829     $expected_expiration = dt_from_string->add( days => floor( ( ( 5 - 1 ) / 3 ) * 2 ) );
1830     test_debarment_on_checkout(
1831         {
1832             item            => $item_1,
1833             library         => $library,
1834             patron          => $patron,
1835             due_date        => $five_days_ago,
1836             expiration_date => $expected_expiration,
1837         }
1838     );
1839
1840     # Use finesCalendar to know if holiday must be skipped to calculate the due date
1841     # We want to charge 2 days every days, with 0 day of grace (to not burn brains)
1842     $rule->finedays(2)->store;
1843     $rule->suspension_chargeperiod(1)->store;
1844     $rule->firstremind(0)->store;
1845     t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
1846
1847     # Adding a holiday 2 days ago
1848     my $calendar = C4::Calendar->new(branchcode => $library->{branchcode});
1849     my $two_days_ago = dt_from_string->subtract( days => 2 );
1850     $calendar->insert_single_holiday(
1851         day             => $two_days_ago->day,
1852         month           => $two_days_ago->month,
1853         year            => $two_days_ago->year,
1854         title           => 'holidayTest-2d',
1855         description     => 'holidayDesc 2 days ago'
1856     );
1857     # With 5 days of overdue, only 4 (x finedays=2) days must charged (one was an holiday)
1858     $expected_expiration = dt_from_string->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) );
1859     test_debarment_on_checkout(
1860         {
1861             item            => $item_1,
1862             library         => $library,
1863             patron          => $patron,
1864             due_date        => $five_days_ago,
1865             expiration_date => $expected_expiration,
1866         }
1867     );
1868
1869     # Adding a holiday 2 days ahead, with finesCalendar=noFinesWhenClosed it should be skipped
1870     my $two_days_ahead = dt_from_string->add( days => 2 );
1871     $calendar->insert_single_holiday(
1872         day             => $two_days_ahead->day,
1873         month           => $two_days_ahead->month,
1874         year            => $two_days_ahead->year,
1875         title           => 'holidayTest+2d',
1876         description     => 'holidayDesc 2 days ahead'
1877     );
1878
1879     # Same as above, but we should skip D+2
1880     $expected_expiration = dt_from_string->add( days => floor( ( ( 5 - 0 - 1 ) / 1 ) * 2 ) + 1 );
1881     test_debarment_on_checkout(
1882         {
1883             item            => $item_1,
1884             library         => $library,
1885             patron          => $patron,
1886             due_date        => $five_days_ago,
1887             expiration_date => $expected_expiration,
1888         }
1889     );
1890
1891     # Adding another holiday, day of expiration date
1892     my $expected_expiration_dt = dt_from_string($expected_expiration);
1893     $calendar->insert_single_holiday(
1894         day             => $expected_expiration_dt->day,
1895         month           => $expected_expiration_dt->month,
1896         year            => $expected_expiration_dt->year,
1897         title           => 'holidayTest_exp',
1898         description     => 'holidayDesc on expiration date'
1899     );
1900     # Expiration date will be the day after
1901     test_debarment_on_checkout(
1902         {
1903             item            => $item_1,
1904             library         => $library,
1905             patron          => $patron,
1906             due_date        => $five_days_ago,
1907             expiration_date => $expected_expiration_dt->clone->add( days => 1 ),
1908         }
1909     );
1910
1911     test_debarment_on_checkout(
1912         {
1913             item            => $item_1,
1914             library         => $library,
1915             patron          => $patron,
1916             return_date     => dt_from_string->add(days => 5),
1917             expiration_date => dt_from_string->add(days => 5 + (5 * 2 - 1) ),
1918         }
1919     );
1920 };
1921
1922 subtest 'CanBookBeIssued + AutoReturnCheckedOutItems' => sub {
1923     plan tests => 2;
1924
1925     my $library = $builder->build( { source => 'Branch' } );
1926     my $patron1 = $builder->build_object(
1927         {
1928             class => 'Koha::Patrons',
1929             value  => {
1930                 branchcode => $library->{branchcode},
1931                 firstname => "Happy",
1932                 surname => "Gilmore",
1933             }
1934         }
1935     );
1936     my $patron2 = $builder->build_object(
1937         {
1938             class => 'Koha::Patrons',
1939             value  => {
1940                 branchcode => $library->{branchcode},
1941                 firstname => "Billy",
1942                 surname => "Madison",
1943             }
1944         }
1945     );
1946
1947     C4::Context->_new_userenv('xxx');
1948     C4::Context->set_userenv(0,0,0,'firstname','surname', $library->{branchcode}, 'Random Library', '', '', '');
1949
1950     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1951     my $biblionumber = $biblioitem->{biblionumber};
1952     my $item = $builder->build(
1953         {   source => 'Item',
1954             value  => {
1955                 homebranch    => $library->{branchcode},
1956                 holdingbranch => $library->{branchcode},
1957                 notforloan    => 0,
1958                 itemlost      => 0,
1959                 withdrawn     => 0,
1960                 biblionumber  => $biblionumber,
1961             }
1962         }
1963     );
1964
1965     my ( $error, $question, $alerts );
1966     my $issue = AddIssue( $patron1->unblessed, $item->{barcode} );
1967
1968     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
1969     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->{barcode} );
1970     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' );
1971
1972     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 1);
1973     ( $error, $question, $alerts ) = CanBookBeIssued( $patron2, $item->{barcode} );
1974     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' );
1975
1976     t::lib::Mocks::mock_preference('AutoReturnCheckedOutItems', 0);
1977 };
1978
1979
1980 subtest 'AddReturn | is_overdue' => sub {
1981     plan tests => 5;
1982
1983     t::lib::Mocks::mock_preference('CalculateFinesOnReturn', 1);
1984     t::lib::Mocks::mock_preference('finesMode', 'production');
1985     t::lib::Mocks::mock_preference('MaxFine', '100');
1986
1987     my $library = $builder->build( { source => 'Branch' } );
1988     my $patron  = $builder->build( { source => 'Borrower', value => { categorycode => $patron_category->{categorycode} } } );
1989
1990     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1991     my $item = $builder->build(
1992         {
1993             source => 'Item',
1994             value  => {
1995                 homebranch    => $library->{branchcode},
1996                 holdingbranch => $library->{branchcode},
1997                 notforloan    => 0,
1998                 itemlost      => 0,
1999                 withdrawn     => 0,
2000                 biblionumber  => $biblioitem->{biblionumber},
2001             }
2002         }
2003     );
2004
2005     Koha::IssuingRules->search->delete;
2006     my $rule = Koha::IssuingRule->new(
2007         {
2008             categorycode => '*',
2009             itemtype     => '*',
2010             branchcode   => '*',
2011             issuelength  => 6,
2012             lengthunit   => 'days',
2013             fine         => 1, # Charge 1 every day of overdue
2014             chargeperiod => 1,
2015         }
2016     );
2017     $rule->store();
2018
2019     my $one_day_ago   = dt_from_string->subtract( days => 1 );
2020     my $five_days_ago = dt_from_string->subtract( days => 5 );
2021     my $ten_days_ago  = dt_from_string->subtract( days => 10 );
2022     $patron = Koha::Patrons->find( $patron->{borrowernumber} );
2023
2024     # No date specify, today will be used
2025     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2026     AddReturn( $item->{barcode}, $library->{branchcode} );
2027     is( int($patron->account->balance()), 10, 'Patron should have a charge of 10 (10 days x 1)' );
2028     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2029
2030     # specify return date 5 days before => no overdue
2031     AddIssue( $patron->unblessed, $item->{barcode}, $five_days_ago ); # date due was 5d ago
2032     AddReturn( $item->{barcode}, $library->{branchcode}, undef, $ten_days_ago );
2033     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2034     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2035
2036     # specify return date 5 days later => overdue
2037     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2038     AddReturn( $item->{barcode}, $library->{branchcode}, undef, $five_days_ago );
2039     is( int($patron->account->balance()), 5, 'AddReturn: pass return_date => overdue' );
2040     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2041
2042     # specify dropbox date 5 days before => no overdue
2043     AddIssue( $patron->unblessed, $item->{barcode}, $five_days_ago ); # date due was 5d ago
2044     AddReturn( $item->{barcode}, $library->{branchcode}, $ten_days_ago );
2045     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue' );
2046     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2047
2048     # specify dropbox date 5 days later => overdue, or... not
2049     AddIssue( $patron->unblessed, $item->{barcode}, $ten_days_ago ); # date due was 10d ago
2050     AddReturn( $item->{barcode}, $library->{branchcode}, $five_days_ago );
2051     is( int($patron->account->balance()), 0, 'AddReturn: pass return_date => no overdue in dropbox mode' ); # FIXME? This is weird, the FU fine is created ( _CalculateAndUpdateFine > C4::Overdues::UpdateFine ) then remove later (in _FixOverduesOnReturn). Looks like it is a feature
2052     Koha::Account::Lines->search({ borrowernumber => $patron->borrowernumber })->delete;
2053 };
2054
2055 subtest '_FixAccountForLostAndReturned' => sub {
2056
2057     plan tests => 5;
2058
2059     t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
2060     t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
2061
2062     my $processfee_amount  = 20;
2063     my $replacement_amount = 99.00;
2064     my $item_type          = $builder->build_object(
2065         {   class => 'Koha::ItemTypes',
2066             value => {
2067                 notforloan         => undef,
2068                 rentalcharge       => 0,
2069                 defaultreplacecost => undef,
2070                 processfee         => $processfee_amount,
2071                 rentalcharge_daily => 0,
2072             }
2073         }
2074     );
2075     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
2076
2077     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Daria' });
2078
2079     subtest 'Full write-off tests' => sub {
2080
2081         plan tests => 10;
2082
2083         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2084         my $manager = $builder->build_object({ class => "Koha::Patrons" });
2085         t::lib::Mocks::mock_userenv({ patron => $manager,branchcode => $manager->branchcode });
2086
2087         my $item = $builder->build_sample_item(
2088             {
2089                 biblionumber     => $biblio->biblionumber,
2090                 library          => $library->branchcode,
2091                 replacementprice => $replacement_amount,
2092                 itype            => $item_type->itemtype,
2093             }
2094         );
2095
2096         AddIssue( $patron->unblessed, $item->barcode );
2097
2098         # Simulate item marked as lost
2099         ModItem( { itemlost => 3 }, $biblio->biblionumber, $item->itemnumber );
2100         LostItem( $item->itemnumber, 1 );
2101
2102         my $processing_fee_lines = Koha::Account::Lines->search(
2103             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
2104         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2105         my $processing_fee_line = $processing_fee_lines->next;
2106         is( $processing_fee_line->amount + 0,
2107             $processfee_amount, 'The right PF amount is generated' );
2108         is( $processing_fee_line->amountoutstanding + 0,
2109             $processfee_amount, 'The right PF amountoutstanding is generated' );
2110
2111         my $lost_fee_lines = Koha::Account::Lines->search(
2112             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
2113         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2114         my $lost_fee_line = $lost_fee_lines->next;
2115         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2116         is( $lost_fee_line->amountoutstanding + 0,
2117             $replacement_amount, 'The right L amountoutstanding is generated' );
2118
2119         my $account = $patron->account;
2120         my $debts   = $account->outstanding_debits;
2121
2122         # Write off the debt
2123         my $credit = $account->add_credit(
2124             {   amount => $account->balance,
2125                 type   => 'writeoff',
2126                 interface => 'test',
2127             }
2128         );
2129         $credit->apply( { debits => $debts, offset_type => 'Writeoff' } );
2130
2131         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2132         is( $credit_return_id, undef, 'No CR account line added' );
2133
2134         $lost_fee_line->discard_changes; # reload from DB
2135         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2136         is( $lost_fee_line->accounttype,
2137             'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2138
2139         is( $patron->account->balance, -0, 'The patron balance is 0, everything was written off' );
2140     };
2141
2142     subtest 'Full payment tests' => sub {
2143
2144         plan tests => 12;
2145
2146         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2147
2148         my $item = $builder->build_sample_item(
2149             {
2150                 biblionumber     => $biblio->biblionumber,
2151                 library          => $library->branchcode,
2152                 replacementprice => $replacement_amount,
2153                 itype            => $item_type->itemtype
2154             }
2155         );
2156
2157         AddIssue( $patron->unblessed, $item->barcode );
2158
2159         # Simulate item marked as lost
2160         ModItem( { itemlost => 1 }, $biblio->biblionumber, $item->itemnumber );
2161         LostItem( $item->itemnumber, 1 );
2162
2163         my $processing_fee_lines = Koha::Account::Lines->search(
2164             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
2165         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2166         my $processing_fee_line = $processing_fee_lines->next;
2167         is( $processing_fee_line->amount + 0,
2168             $processfee_amount, 'The right PF amount is generated' );
2169         is( $processing_fee_line->amountoutstanding + 0,
2170             $processfee_amount, 'The right PF amountoutstanding is generated' );
2171
2172         my $lost_fee_lines = Koha::Account::Lines->search(
2173             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
2174         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2175         my $lost_fee_line = $lost_fee_lines->next;
2176         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2177         is( $lost_fee_line->amountoutstanding + 0,
2178             $replacement_amount, 'The right L amountountstanding is generated' );
2179
2180         my $account = $patron->account;
2181         my $debts   = $account->outstanding_debits;
2182
2183         # Write off the debt
2184         my $credit = $account->add_credit(
2185             {   amount => $account->balance,
2186                 type   => 'payment',
2187                 interface => 'test',
2188             }
2189         );
2190         $credit->apply( { debits => $debts, offset_type => 'Payment' } );
2191
2192         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2193         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2194
2195         is( $credit_return->accounttype, 'CR', 'An account line of type CR is added' );
2196         is( $credit_return->amount + 0,
2197             -99.00, 'The account line of type CR has an amount of -99' );
2198         is( $credit_return->amountoutstanding + 0,
2199             -99.00, 'The account line of type CR has an amountoutstanding of -99' );
2200
2201         $lost_fee_line->discard_changes;
2202         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2203         is( $lost_fee_line->accounttype,
2204             'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2205
2206         is( $patron->account->balance,
2207             -99, 'The patron balance is -99, a credit that equals the lost fee payment' );
2208     };
2209
2210     subtest 'Test without payment or write off' => sub {
2211
2212         plan tests => 12;
2213
2214         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2215
2216         my $item = $builder->build_sample_item(
2217             {
2218                 biblionumber     => $biblio->biblionumber,
2219                 library          => $library->branchcode,
2220                 replacementprice => 23.00,
2221                 replacementprice => $replacement_amount,
2222                 itype            => $item_type->itemtype
2223             }
2224         );
2225
2226         AddIssue( $patron->unblessed, $item->barcode );
2227
2228         # Simulate item marked as lost
2229         ModItem( { itemlost => 3 }, $biblio->biblionumber, $item->itemnumber );
2230         LostItem( $item->itemnumber, 1 );
2231
2232         my $processing_fee_lines = Koha::Account::Lines->search(
2233             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
2234         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2235         my $processing_fee_line = $processing_fee_lines->next;
2236         is( $processing_fee_line->amount + 0,
2237             $processfee_amount, 'The right PF amount is generated' );
2238         is( $processing_fee_line->amountoutstanding + 0,
2239             $processfee_amount, 'The right PF amountoutstanding is generated' );
2240
2241         my $lost_fee_lines = Koha::Account::Lines->search(
2242             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
2243         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2244         my $lost_fee_line = $lost_fee_lines->next;
2245         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2246         is( $lost_fee_line->amountoutstanding + 0,
2247             $replacement_amount, 'The right L amountountstanding is generated' );
2248
2249         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2250         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2251
2252         is( $credit_return->accounttype, 'CR', 'An account line of type CR is added' );
2253         is( $credit_return->amount + 0, -99.00, 'The account line of type CR has an amount of -99' );
2254         is( $credit_return->amountoutstanding + 0, 0, 'The account line of type CR has an amountoutstanding of 0' );
2255
2256         $lost_fee_line->discard_changes;
2257         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2258         is( $lost_fee_line->accounttype, 'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2259
2260         is( $patron->account->balance, 20, 'The patron balance is 20, still owes the processing fee' );
2261     };
2262
2263     subtest 'Test with partial payement and write off, and remaining debt' => sub {
2264
2265         plan tests => 15;
2266
2267         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2268         my $item = $builder->build_sample_item(
2269             {
2270                 biblionumber     => $biblio->biblionumber,
2271                 library          => $library->branchcode,
2272                 replacementprice => $replacement_amount,
2273                 itype            => $item_type->itemtype
2274             }
2275         );
2276
2277         AddIssue( $patron->unblessed, $item->barcode );
2278
2279         # Simulate item marked as lost
2280         ModItem( { itemlost => 1 }, $biblio->biblionumber, $item->itemnumber );
2281         LostItem( $item->itemnumber, 1 );
2282
2283         my $processing_fee_lines = Koha::Account::Lines->search(
2284             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'PF' } );
2285         is( $processing_fee_lines->count, 1, 'Only one processing fee produced' );
2286         my $processing_fee_line = $processing_fee_lines->next;
2287         is( $processing_fee_line->amount + 0,
2288             $processfee_amount, 'The right PF amount is generated' );
2289         is( $processing_fee_line->amountoutstanding + 0,
2290             $processfee_amount, 'The right PF amountoutstanding is generated' );
2291
2292         my $lost_fee_lines = Koha::Account::Lines->search(
2293             { borrowernumber => $patron->id, itemnumber => $item->itemnumber, accounttype => 'L' } );
2294         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2295         my $lost_fee_line = $lost_fee_lines->next;
2296         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2297         is( $lost_fee_line->amountoutstanding + 0,
2298             $replacement_amount, 'The right L amountountstanding is generated' );
2299
2300         my $account = $patron->account;
2301         is( $account->balance, $processfee_amount + $replacement_amount, 'Balance is PF + L' );
2302
2303         # Partially pay fee
2304         my $payment_amount = 27;
2305         my $payment        = $account->add_credit(
2306             {   amount => $payment_amount,
2307                 type   => 'payment',
2308                 interface => 'test',
2309             }
2310         );
2311
2312         $payment->apply( { debits => $lost_fee_lines->reset, offset_type => 'Payment' } );
2313
2314         # Partially write off fee
2315         my $write_off_amount = 25;
2316         my $write_off        = $account->add_credit(
2317             {   amount => $write_off_amount,
2318                 type   => 'writeoff',
2319                 interface => 'test',
2320             }
2321         );
2322         $write_off->apply( { debits => $lost_fee_lines->reset, offset_type => 'Writeoff' } );
2323
2324         is( $account->balance,
2325             $processfee_amount + $replacement_amount - $payment_amount - $write_off_amount,
2326             'Payment and write off applied'
2327         );
2328
2329         # Store the amountoutstanding value
2330         $lost_fee_line->discard_changes;
2331         my $outstanding = $lost_fee_line->amountoutstanding;
2332
2333         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item->itemnumber, $patron->id );
2334         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2335
2336         is( $account->balance, $processfee_amount - $payment_amount, 'Balance is PF - payment (CR)' );
2337
2338         $lost_fee_line->discard_changes;
2339         is( $lost_fee_line->amountoutstanding + 0, 0, 'Lost fee has no outstanding amount' );
2340         is( $lost_fee_line->accounttype,
2341             'LR', 'Lost fee now has account type of LR ( Lost Returned )' );
2342
2343         is( $credit_return->accounttype, 'CR', 'An account line of type CR is added' );
2344         is( $credit_return->amount + 0,
2345             ($payment_amount + $outstanding ) * -1,
2346             'The account line of type CR has an amount equal to the payment + outstanding'
2347         );
2348         is( $credit_return->amountoutstanding + 0,
2349             $payment_amount * -1,
2350             'The account line of type CR has an amountoutstanding equal to the payment'
2351         );
2352
2353         is( $account->balance,
2354             $processfee_amount - $payment_amount,
2355             'The patron balance is the difference between the PF and the credit'
2356         );
2357     };
2358
2359     subtest 'Partial payement, existing debits and AccountAutoReconcile' => sub {
2360
2361         plan tests => 8;
2362
2363         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
2364         my $barcode = 'KD123456793';
2365         my $replacement_amount = 100;
2366         my $processfee_amount  = 20;
2367
2368         my $item_type          = $builder->build_object(
2369             {   class => 'Koha::ItemTypes',
2370                 value => {
2371                     notforloan         => undef,
2372                     rentalcharge       => 0,
2373                     defaultreplacecost => undef,
2374                     processfee         => 0,
2375                     rentalcharge_daily => 0,
2376                 }
2377             }
2378         );
2379         my ( undef, undef, $item_id ) = AddItem(
2380             {   homebranch       => $library->branchcode,
2381                 holdingbranch    => $library->branchcode,
2382                 barcode          => $barcode,
2383                 replacementprice => $replacement_amount,
2384                 itype            => $item_type->itemtype
2385             },
2386             $biblio->biblionumber
2387         );
2388
2389         AddIssue( $patron->unblessed, $barcode );
2390
2391         # Simulate item marked as lost
2392         ModItem( { itemlost => 1 }, $biblio->biblionumber, $item_id );
2393         LostItem( $item_id, 1 );
2394
2395         my $lost_fee_lines = Koha::Account::Lines->search(
2396             { borrowernumber => $patron->id, itemnumber => $item_id, accounttype => 'L' } );
2397         is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
2398         my $lost_fee_line = $lost_fee_lines->next;
2399         is( $lost_fee_line->amount + 0, $replacement_amount, 'The right L amount is generated' );
2400         is( $lost_fee_line->amountoutstanding + 0,
2401             $replacement_amount, 'The right L amountountstanding is generated' );
2402
2403         my $account = $patron->account;
2404         is( $account->balance, $replacement_amount, 'Balance is L' );
2405
2406         # Partially pay fee
2407         my $payment_amount = 27;
2408         my $payment        = $account->add_credit(
2409             {   amount => $payment_amount,
2410                 type   => 'payment',
2411                 interface => 'test',
2412             }
2413         );
2414         $payment->apply({ debits => $lost_fee_lines->reset, offset_type => 'Payment' });
2415
2416         is( $account->balance,
2417             $replacement_amount - $payment_amount,
2418             'Payment applied'
2419         );
2420
2421         my $manual_debit_amount = 80;
2422         $account->add_debit( { amount => $manual_debit_amount, type => 'fine', interface =>'test' } );
2423
2424         is( $account->balance, $manual_debit_amount + $replacement_amount - $payment_amount, 'Manual debit applied' );
2425
2426         t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
2427
2428         my $credit_return_id = C4::Circulation::_FixAccountForLostAndReturned( $item_id, $patron->id );
2429         my $credit_return = Koha::Account::Lines->find($credit_return_id);
2430
2431         is( $account->balance, $manual_debit_amount - $payment_amount, 'Balance is PF - payment (CR)' );
2432
2433         my $manual_debit = Koha::Account::Lines->search({ borrowernumber => $patron->id, accounttype => 'FU' })->next;
2434         is( $manual_debit->amountoutstanding + 0, $manual_debit_amount - $payment_amount, 'reconcile_balance was called' );
2435     };
2436 };
2437
2438 subtest '_FixOverduesOnReturn' => sub {
2439     plan tests => 6;
2440
2441     my $biblio = $builder->build_sample_biblio({ author => 'Hall, Kylie' });
2442
2443     my $branchcode  = $library2->{branchcode};
2444
2445     my $item = $builder->build_sample_item(
2446         {
2447             biblionumber     => $biblio->biblionumber,
2448             library          => $branchcode,
2449             replacementprice => 99.00,
2450             itype            => $itemtype,
2451         }
2452     );
2453
2454     my $patron = $builder->build( { source => 'Borrower' } );
2455
2456     ## Start with basic call, should just close out the open fine
2457     my $accountline = Koha::Account::Line->new(
2458         {
2459             borrowernumber => $patron->{borrowernumber},
2460             accounttype    => 'FU',
2461             itemnumber     => $item->itemnumber,
2462             amount => 99.00,
2463             amountoutstanding => 99.00,
2464             interface => 'test',
2465         }
2466     )->store();
2467
2468     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber );
2469
2470     $accountline->_result()->discard_changes();
2471
2472     is( $accountline->amountoutstanding, '99.000000', 'Fine has the same amount outstanding as previously' );
2473     is( $accountline->accounttype, 'F', 'Open fine ( account type FU ) has been closed out ( account type F )');
2474
2475
2476     ## Run again, with exemptfine enabled
2477     $accountline->set(
2478         {
2479             accounttype    => 'FU',
2480             amountoutstanding => 99.00,
2481         }
2482     )->store();
2483
2484     C4::Circulation::_FixOverduesOnReturn( $patron->{borrowernumber}, $item->itemnumber, 1 );
2485
2486     $accountline->_result()->discard_changes();
2487     my $offset = Koha::Account::Offsets->search({ debit_id => $accountline->id, type => 'Forgiven' })->next();
2488
2489     is( $accountline->amountoutstanding + 0, 0, 'Fine has been reduced to 0' );
2490     is( $accountline->accounttype, 'FFOR', 'Open fine ( account type FU ) has been set to fine forgiven ( account type FFOR )');
2491     is( ref $offset, "Koha::Account::Offset", "Found matching offset for fine reduction via forgiveness" );
2492     is( $offset->amount, '-99.000000', "Amount of offset is correct" );
2493 };
2494
2495 subtest 'Set waiting flag' => sub {
2496     plan tests => 4;
2497
2498     my $library_1 = $builder->build( { source => 'Branch' } );
2499     my $patron_1  = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2500     my $library_2 = $builder->build( { source => 'Branch' } );
2501     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2502
2503     my $biblio = $builder->build( { source => 'Biblio' } );
2504     my $biblioitem = $builder->build( { source => 'Biblioitem', value => { biblionumber => $biblio->{biblionumber} } } );
2505
2506     my $item = $builder->build(
2507         {
2508             source => 'Item',
2509             value  => {
2510                 homebranch    => $library_1->{branchcode},
2511                 holdingbranch => $library_1->{branchcode},
2512                 notforloan    => 0,
2513                 itemlost      => 0,
2514                 withdrawn     => 0,
2515                 biblionumber  => $biblioitem->{biblionumber},
2516             }
2517         }
2518     );
2519
2520     set_userenv( $library_2 );
2521     my $reserve_id = AddReserve(
2522         $library_2->{branchcode}, $patron_2->{borrowernumber}, $biblioitem->{biblionumber},
2523         '', 1, undef, undef, '', undef, $item->{itemnumber},
2524     );
2525
2526     set_userenv( $library_1 );
2527     my $do_transfer = 1;
2528     my ( $res, $rr ) = AddReturn( $item->{barcode}, $library_1->{branchcode} );
2529     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
2530     my $hold = Koha::Holds->find( $reserve_id );
2531     is( $hold->found, 'T', 'Hold is in transit' );
2532
2533     my ( $status ) = CheckReserves($item->{itemnumber});
2534     is( $status, 'Reserved', 'Hold is not waiting yet');
2535
2536     set_userenv( $library_2 );
2537     $do_transfer = 0;
2538     AddReturn( $item->{barcode}, $library_2->{branchcode} );
2539     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
2540     $hold = Koha::Holds->find( $reserve_id );
2541     is( $hold->found, 'W', 'Hold is waiting' );
2542     ( $status ) = CheckReserves($item->{itemnumber});
2543     is( $status, 'Waiting', 'Now the hold is waiting');
2544 };
2545
2546 subtest 'Cancel transfers on lost items' => sub {
2547     plan tests => 5;
2548     my $library_1 = $builder->build( { source => 'Branch' } );
2549     my $patron_1 = $builder->build( { source => 'Borrower', value => { branchcode => $library_1->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2550     my $library_2 = $builder->build( { source => 'Branch' } );
2551     my $patron_2  = $builder->build( { source => 'Borrower', value => { branchcode => $library_2->{branchcode}, categorycode => $patron_category->{categorycode} } } );
2552     my $biblio = $builder->build( { source => 'Biblio' } );
2553     my $biblioitem = $builder->build( { source => 'Biblioitem', value => { biblionumber => $biblio->{biblionumber} } } );
2554     my $item = $builder->build(
2555         {
2556             source => 'Item',
2557             value => {
2558                 homebranch => $library_1->{branchcode},
2559                 holdingbranch => $library_1->{branchcode},
2560                 notforloan => 0,
2561                 itemlost => 0,
2562                 withdrawn => 0,
2563                 biblionumber => $biblioitem->{biblionumber},
2564             }
2565         }
2566     );
2567
2568     set_userenv( $library_2 );
2569     my $reserve_id = AddReserve(
2570         $library_2->{branchcode}, $patron_2->{borrowernumber}, $biblioitem->{biblionumber}, '', 1, undef, undef, '', undef, $item->{itemnumber},
2571     );
2572
2573     #Return book and add transfer
2574     set_userenv( $library_1 );
2575     my $do_transfer = 1;
2576     my ( $res, $rr ) = AddReturn( $item->{barcode}, $library_1->{branchcode} );
2577     ModReserveAffect( $item->{itemnumber}, undef, $do_transfer, $reserve_id );
2578     C4::Circulation::transferbook( $library_2->{branchcode}, $item->{barcode} );
2579     my $hold = Koha::Holds->find( $reserve_id );
2580     is( $hold->found, 'T', 'Hold is in transit' );
2581
2582     #Check transfer exists and the items holding branch is the transfer destination branch before marking it as lost
2583     my ($datesent,$frombranch,$tobranch) = GetTransfers($item->{itemnumber});
2584     is( $tobranch, $library_2->{branchcode}, 'The transfer record exists in the branchtransfers table');
2585     my $itemcheck = Koha::Items->find($item->{itemnumber});
2586     is( $itemcheck->holdingbranch, $library_2->{branchcode}, 'Items holding branch is the transfers destination branch before it is marked as lost' );
2587
2588     #Simulate item being marked as lost and confirm the transfer is deleted and the items holding branch is the transfers source branch
2589     ModItem( { itemlost => 1 }, $biblio->{biblionumber}, $item->{itemnumber} );
2590     LostItem( $item->{itemnumber}, 'test', 1 );
2591     ($datesent,$frombranch,$tobranch) = GetTransfers($item->{itemnumber});
2592     is( $tobranch, undef, 'The transfer on the lost item has been deleted as the LostItemCancelOutstandingTransfer is enabled');
2593     $itemcheck = Koha::Items->find($item->{itemnumber});
2594     is( $itemcheck->holdingbranch, $library_1->{branchcode}, 'Lost item with cancelled hold has holding branch equallying the transfers source branch' );
2595 };
2596
2597 subtest 'CanBookBeIssued | is_overdue' => sub {
2598     plan tests => 3;
2599
2600     # Set a simple circ policy
2601     $dbh->do('DELETE FROM issuingrules');
2602     $dbh->do(
2603     q{INSERT INTO issuingrules (categorycode, branchcode, itemtype, reservesallowed,
2604                                     issuelength, lengthunit,
2605                                     renewalsallowed, renewalperiod,
2606                                     norenewalbefore, auto_renew,
2607                                     fine, chargeperiod)
2608           VALUES (?, ?, ?, ?,
2609                   ?, ?,
2610                   ?, ?,
2611                   ?, ?,
2612                   ?, ?
2613                  )
2614         },
2615         {},
2616         '*',   '*', '*', 25,
2617         14,  'days',
2618         1,     7,
2619         undef, 0,
2620         .10,   1
2621     );
2622
2623     my $five_days_go = output_pref({ dt => dt_from_string->add( days => 5 ), dateonly => 1});
2624     my $ten_days_go  = output_pref({ dt => dt_from_string->add( days => 10), dateonly => 1 });
2625     my $library = $builder->build( { source => 'Branch' } );
2626     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } );
2627
2628     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
2629     my $item = $builder->build(
2630         {
2631             source => 'Item',
2632             value  => {
2633                 homebranch    => $library->{branchcode},
2634                 holdingbranch => $library->{branchcode},
2635                 notforloan    => 0,
2636                 itemlost      => 0,
2637                 withdrawn     => 0,
2638                 biblionumber  => $biblioitem->{biblionumber},
2639             }
2640         }
2641     );
2642
2643     my $issue = AddIssue( $patron->unblessed, $item->{barcode}, $five_days_go ); # date due was 10d ago
2644     my $actualissue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
2645     is( output_pref({ str => $actualissue->date_due, dateonly => 1}), $five_days_go, "First issue works");
2646     my ($issuingimpossible, $needsconfirmation) = CanBookBeIssued($patron,$item->{barcode},$ten_days_go, undef, undef, undef);
2647     is( $needsconfirmation->{RENEW_ISSUE}, 1, "This is a renewal");
2648     is( $needsconfirmation->{TOO_MANY}, undef, "Not too many, is a renewal");
2649 };
2650
2651 subtest 'ItemsDeniedRenewal preference' => sub {
2652     plan tests => 18;
2653
2654     C4::Context->set_preference('ItemsDeniedRenewal','');
2655
2656     my $idr_lib = $builder->build_object({ class => 'Koha::Libraries'});
2657     $dbh->do(
2658         q{
2659         INSERT INTO issuingrules ( categorycode, branchcode, itemtype, reservesallowed, issuelength, lengthunit, renewalsallowed, renewalperiod,
2660                     norenewalbefore, auto_renew, fine, chargeperiod ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2661         },
2662         {},
2663         '*', $idr_lib->branchcode, '*', 25,
2664         14,  'days',
2665         10,   7,
2666         undef,  0,
2667         .10, 1
2668     );
2669
2670     my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
2671         homebranch => $idr_lib->branchcode,
2672         withdrawn => 1,
2673         itype => 'HIDE',
2674         location => 'PROC',
2675         itemcallnumber => undef,
2676         itemnotes => "",
2677         }
2678     });
2679     my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
2680         homebranch => $idr_lib->branchcode,
2681         withdrawn => 0,
2682         itype => 'NOHIDE',
2683         location => 'NOPROC'
2684         }
2685     });
2686
2687     my $idr_borrower = $builder->build_object({ class => 'Koha::Patrons', value=> {
2688         branchcode => $idr_lib->branchcode,
2689         }
2690     });
2691     my $future = dt_from_string->add( days => 1 );
2692     my $deny_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
2693         returndate => undef,
2694         renewals => 0,
2695         auto_renew => 0,
2696         borrowernumber => $idr_borrower->borrowernumber,
2697         itemnumber => $deny_book->itemnumber,
2698         onsite_checkout => 0,
2699         date_due => $future,
2700         }
2701     });
2702     my $allow_issue = $builder->build_object({ class => 'Koha::Checkouts', value => {
2703         returndate => undef,
2704         renewals => 0,
2705         auto_renew => 0,
2706         borrowernumber => $idr_borrower->borrowernumber,
2707         itemnumber => $allow_book->itemnumber,
2708         onsite_checkout => 0,
2709         date_due => $future,
2710         }
2711     });
2712
2713     my $idr_rules;
2714
2715     my ( $idr_mayrenew, $idr_error ) =
2716     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2717     is( $idr_mayrenew, 1, 'Renewal allowed when no rules' );
2718     is( $idr_error, undef, 'Renewal allowed when no rules' );
2719
2720     $idr_rules="withdrawn: [1]";
2721
2722     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2723     ( $idr_mayrenew, $idr_error ) =
2724     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2725     is( $idr_mayrenew, 0, 'Renewal blocked when 1 rules (withdrawn)' );
2726     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 1 rule (withdrawn)' );
2727     ( $idr_mayrenew, $idr_error ) =
2728     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
2729     is( $idr_mayrenew, 1, 'Renewal allowed when 1 rules not matched (withdrawn)' );
2730     is( $idr_error, undef, 'Renewal allowed when 1 rules not matched (withdrawn)' );
2731
2732     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
2733
2734     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2735     ( $idr_mayrenew, $idr_error ) =
2736     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2737     is( $idr_mayrenew, 0, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
2738     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 2 rules matched (withdrawn,itype)' );
2739     ( $idr_mayrenew, $idr_error ) =
2740     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
2741     is( $idr_mayrenew, 1, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
2742     is( $idr_error, undef, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
2743
2744     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
2745
2746     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2747     ( $idr_mayrenew, $idr_error ) =
2748     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2749     is( $idr_mayrenew, 0, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
2750     is( $idr_error, 'item_denied_renewal', 'Renewal blocked when 3 rules matched (withdrawn,itype, location)' );
2751     ( $idr_mayrenew, $idr_error ) =
2752     CanBookBeRenewed( $idr_borrower->borrowernumber, $allow_issue->itemnumber );
2753     is( $idr_mayrenew, 1, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
2754     is( $idr_error, undef, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
2755
2756     $idr_rules="itemcallnumber: [NULL]";
2757     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2758     ( $idr_mayrenew, $idr_error ) =
2759     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2760     is( $idr_mayrenew, 0, 'Renewal blocked for undef when NULL in pref' );
2761     $idr_rules="itemcallnumber: ['']";
2762     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2763     ( $idr_mayrenew, $idr_error ) =
2764     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2765     is( $idr_mayrenew, 1, 'Renewal not blocked for undef when "" in pref' );
2766
2767     $idr_rules="itemnotes: [NULL]";
2768     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2769     ( $idr_mayrenew, $idr_error ) =
2770     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2771     is( $idr_mayrenew, 1, 'Renewal not blocked for "" when NULL in pref' );
2772     $idr_rules="itemnotes: ['']";
2773     C4::Context->set_preference('ItemsDeniedRenewal',$idr_rules);
2774     ( $idr_mayrenew, $idr_error ) =
2775     CanBookBeRenewed( $idr_borrower->borrowernumber, $deny_issue->itemnumber );
2776     is( $idr_mayrenew, 0, 'Renewal blocked for empty string when "" in pref' );
2777 };
2778
2779 subtest 'CanBookBeIssued | item-level_itypes=biblio' => sub {
2780     plan tests => 2;
2781
2782     t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
2783     my $library = $builder->build( { source => 'Branch' } );
2784     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
2785
2786     my $itemtype = $builder->build(
2787         {
2788             source => 'Itemtype',
2789             value  => { notforloan => undef, }
2790         }
2791     );
2792
2793     my $biblioitem = $builder->build( { source => 'Biblioitem', value => { itemtype => $itemtype->{itemtype} } } );
2794     my $item = $builder->build_object(
2795         {
2796             class => 'Koha::Items',
2797             value  => {
2798                 homebranch    => $library->{branchcode},
2799                 holdingbranch => $library->{branchcode},
2800                 notforloan    => 0,
2801                 itemlost      => 0,
2802                 withdrawn     => 0,
2803                 biblionumber  => $biblioitem->{biblionumber},
2804                 biblioitemnumber => $biblioitem->{biblioitemnumber},
2805             }
2806         }
2807     )->store;
2808
2809     my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2810     is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
2811     is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
2812 };
2813
2814 subtest 'CanBookBeIssued | notforloan' => sub {
2815     plan tests => 2;
2816
2817     t::lib::Mocks::mock_preference('AllowNotForLoanOverride', 0);
2818
2819     my $library = $builder->build( { source => 'Branch' } );
2820     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { categorycode => $patron_category->{categorycode} } } )->store;
2821
2822     my $itemtype = $builder->build(
2823         {
2824             source => 'Itemtype',
2825             value  => { notforloan => undef, }
2826         }
2827     );
2828
2829     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
2830     my $item = $builder->build_object(
2831         {
2832             class => 'Koha::Items',
2833             value  => {
2834                 homebranch    => $library->{branchcode},
2835                 holdingbranch => $library->{branchcode},
2836                 notforloan    => 0,
2837                 itemlost      => 0,
2838                 withdrawn     => 0,
2839                 itype         => $itemtype->{itemtype},
2840                 biblionumber  => $biblioitem->{biblionumber},
2841                 biblioitemnumber => $biblioitem->{biblioitemnumber},
2842             }
2843         }
2844     )->store;
2845
2846     my ( $issuingimpossible, $needsconfirmation );
2847
2848
2849     subtest 'item-level_itypes = 1' => sub {
2850         plan tests => 6;
2851
2852         t::lib::Mocks::mock_preference('item-level_itypes', 1); # item
2853         # Is for loan at item type and item level
2854         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2855         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
2856         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
2857
2858         # not for loan at item type level
2859         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
2860         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2861         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2862         is_deeply(
2863             $issuingimpossible,
2864             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
2865             'Item can not be issued, not for loan at item type level'
2866         );
2867
2868         # not for loan at item level
2869         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
2870         $item->notforloan( 1 )->store;
2871         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2872         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2873         is_deeply(
2874             $issuingimpossible,
2875             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
2876             'Item can not be issued, not for loan at item type level'
2877         );
2878     };
2879
2880     subtest 'item-level_itypes = 0' => sub {
2881         plan tests => 6;
2882
2883         t::lib::Mocks::mock_preference('item-level_itypes', 0); # biblio
2884
2885         # We set another itemtype for biblioitem
2886         my $itemtype = $builder->build(
2887             {
2888                 source => 'Itemtype',
2889                 value  => { notforloan => undef, }
2890             }
2891         );
2892
2893         # for loan at item type and item level
2894         $item->notforloan(0)->store;
2895         $item->biblioitem->itemtype($itemtype->{itemtype})->store;
2896         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2897         is_deeply( $needsconfirmation, {}, 'Item can be issued to this patron' );
2898         is_deeply( $issuingimpossible, {}, 'Item can be issued to this patron' );
2899
2900         # not for loan at item type level
2901         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(1)->store;
2902         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2903         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2904         is_deeply(
2905             $issuingimpossible,
2906             { NOT_FOR_LOAN => 1, itemtype_notforloan => $itemtype->{itemtype} },
2907             'Item can not be issued, not for loan at item type level'
2908         );
2909
2910         # not for loan at item level
2911         Koha::ItemTypes->find( $itemtype->{itemtype} )->notforloan(undef)->store;
2912         $item->notforloan( 1 )->store;
2913         ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, undef, undef, undef, undef );
2914         is_deeply( $needsconfirmation, {}, 'No confirmation needed, AllowNotForLoanOverride=0' );
2915         is_deeply(
2916             $issuingimpossible,
2917             { NOT_FOR_LOAN => 1, item_notforloan => 1 },
2918             'Item can not be issued, not for loan at item type level'
2919         );
2920     };
2921
2922     # TODO test with AllowNotForLoanOverride = 1
2923 };
2924
2925 subtest 'AddReturn should clear items.onloan for unissued items' => sub {
2926     plan tests => 1;
2927
2928     t::lib::Mocks::mock_preference( "AllowReturnToBranch", 'anywhere' );
2929     my $item = $builder->build_object({ class => 'Koha::Items', value  => { onloan => '2018-01-01' }});
2930     AddReturn( $item->barcode, $item->homebranch );
2931     $item->discard_changes; # refresh
2932     is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
2933 };
2934
2935 $schema->storage->txn_rollback;
2936 C4::Context->clear_syspref_cache();
2937 $cache->clear_from_cache('single_holidays');
2938
2939 subtest 'AddRenewal and AddIssuingCharge tests' => sub {
2940
2941     plan tests => 13;
2942
2943     $schema->storage->txn_begin;
2944
2945     t::lib::Mocks::mock_preference('item-level_itypes', 1);
2946
2947     my $issuing_charges = 15;
2948     my $title   = 'A title';
2949     my $author  = 'Author, An';
2950     my $barcode = 'WHATARETHEODDS';
2951
2952     my $circ = Test::MockModule->new('C4::Circulation');
2953     $circ->mock(
2954         'GetIssuingCharges',
2955         sub {
2956             return $issuing_charges;
2957         }
2958     );
2959
2960     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
2961     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes', value => { rentalcharge_daily => 0.00 }});
2962     my $patron   = $builder->build_object({
2963         class => 'Koha::Patrons',
2964         value => { branchcode => $library->id }
2965     });
2966
2967     my $biblio = $builder->build_sample_biblio({ title=> $title, author => $author });
2968     my ( undef, undef, $item_id ) = AddItem(
2969         {
2970             homebranch       => $library->id,
2971             holdingbranch    => $library->id,
2972             barcode          => $barcode,
2973             replacementprice => 23.00,
2974             itype            => $itemtype->id
2975         },
2976         $biblio->biblionumber
2977     );
2978     my $item = Koha::Items->find( $item_id );
2979
2980     my $context = Test::MockModule->new('C4::Context');
2981     $context->mock( userenv => { branch => $library->id } );
2982
2983     # Check the item out
2984     AddIssue( $patron->unblessed, $item->barcode );
2985     t::lib::Mocks::mock_preference( 'RenewalLog', 0 );
2986     my $date = output_pref( { dt => dt_from_string(), datenonly => 1, dateformat => 'iso' } );
2987     my %params_renewal = (
2988         timestamp => { -like => $date . "%" },
2989         module => "CIRCULATION",
2990         action => "RENEWAL",
2991     );
2992     my $old_log_size = Koha::ActionLogs->count( \%params_renewal );;
2993     AddRenewal( $patron->id, $item->id, $library->id );
2994     my $new_log_size = Koha::ActionLogs->count( \%params_renewal );
2995     is( $new_log_size, $old_log_size, 'renew log not added because of the syspref RenewalLog' );
2996
2997     my $checkouts = $patron->checkouts;
2998     # The following will fail if run on 00:00:00
2999     unlike ( $checkouts->next->lastreneweddate, qr/00:00:00/, 'AddRenewal should set the renewal date with the time part');
3000
3001     t::lib::Mocks::mock_preference( 'RenewalLog', 1 );
3002     $date = output_pref( { dt => dt_from_string(), datenonly => 1, dateformat => 'iso' } );
3003     $old_log_size = Koha::ActionLogs->count( \%params_renewal );
3004     AddRenewal( $patron->id, $item->id, $library->id );
3005     $new_log_size = Koha::ActionLogs->count( \%params_renewal );
3006     is( $new_log_size, $old_log_size + 1, 'renew log successfully added' );
3007
3008     my $lines = Koha::Account::Lines->search({
3009         borrowernumber => $patron->id,
3010         itemnumber     => $item->id
3011     });
3012
3013     is( $lines->count, 3 );
3014
3015     my $line = $lines->next;
3016     is( $line->accounttype, 'Rent',       'The issuing charge generates an accountline' );
3017     is( $line->branchcode,  $library->id, 'AddIssuingCharge correctly sets branchcode' );
3018     is( $line->description, 'Rental',     'AddIssuingCharge set a hardcoded description for the accountline' );
3019
3020     $line = $lines->next;
3021     is( $line->accounttype, 'Rent', 'Fine on renewed item is closed out properly' );
3022     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
3023     is( $line->description, "Renewal of Rental Item $title $barcode", 'AddRenewal set a hardcoded description for the accountline' );
3024
3025     $line = $lines->next;
3026     is( $line->accounttype, 'Rent', 'Fine on renewed item is closed out properly' );
3027     is( $line->branchcode,  $library->id, 'AddRenewal correctly sets branchcode' );
3028     is( $line->description, "Renewal of Rental Item $title $barcode", 'AddRenewal set a hardcoded description for the accountline' );
3029
3030     $schema->storage->txn_rollback;
3031 };
3032
3033 subtest 'ProcessOfflinePayment() tests' => sub {
3034
3035     plan tests => 4;
3036
3037     $schema->storage->txn_begin;
3038
3039     my $amount = 123;
3040
3041     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
3042     my $library = $builder->build_object({ class => 'Koha::Libraries' });
3043     my $result  = C4::Circulation::ProcessOfflinePayment({ cardnumber => $patron->cardnumber, amount => $amount, branchcode => $library->id });
3044
3045     is( $result, 'Success.', 'The right string is returned' );
3046
3047     my $lines = $patron->account->lines;
3048     is( $lines->count, 1, 'line created correctly');
3049
3050     my $line = $lines->next;
3051     is( $line->amount+0, $amount * -1, 'amount picked from params' );
3052     is( $line->branchcode, $library->id, 'branchcode set correctly' );
3053
3054     $schema->storage->txn_rollback;
3055 };
3056
3057
3058
3059 sub set_userenv {
3060     my ( $library ) = @_;
3061     t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
3062 }
3063
3064 sub str {
3065     my ( $error, $question, $alert ) = @_;
3066     my $s;
3067     $s  = %$error    ? ' (error: '    . join( ' ', keys %$error    ) . ')' : '';
3068     $s .= %$question ? ' (question: ' . join( ' ', keys %$question ) . ')' : '';
3069     $s .= %$alert    ? ' (alert: '    . join( ' ', keys %$alert    ) . ')' : '';
3070     return $s;
3071 }
3072
3073 sub test_debarment_on_checkout {
3074     my ($params) = @_;
3075     my $item     = $params->{item};
3076     my $library  = $params->{library};
3077     my $patron   = $params->{patron};
3078     my $due_date = $params->{due_date} || dt_from_string;
3079     my $return_date = $params->{return_date} || dt_from_string;
3080     my $expected_expiration_date = $params->{expiration_date};
3081
3082     $expected_expiration_date = output_pref(
3083         {
3084             dt         => $expected_expiration_date,
3085             dateformat => 'sql',
3086             dateonly   => 1,
3087         }
3088     );
3089     my @caller      = caller;
3090     my $line_number = $caller[2];
3091     AddIssue( $patron, $item->{barcode}, $due_date );
3092
3093     my ( undef, $message ) = AddReturn( $item->{barcode}, $library->{branchcode}, undef, $return_date );
3094     is( $message->{WasReturned} && exists $message->{Debarred}, 1, 'AddReturn must have debarred the patron' )
3095         or diag('AddReturn returned message ' . Dumper $message );
3096     my $debarments = Koha::Patron::Debarments::GetDebarments(
3097         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
3098     is( scalar(@$debarments), 1, 'Test at line ' . $line_number );
3099
3100     is( $debarments->[0]->{expiration},
3101         $expected_expiration_date, 'Test at line ' . $line_number );
3102     Koha::Patron::Debarments::DelUniqueDebarment(
3103         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
3104 };
3105
3106 subtest 'Incremented fee tests' => sub {
3107     plan tests => 11;
3108
3109     t::lib::Mocks::mock_preference('item-level_itypes', 1);
3110
3111     my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store;
3112
3113     my $module = new Test::MockModule('C4::Context');
3114     $module->mock('userenv', sub { { branch => $library->id } });
3115
3116     my $patron = $builder->build_object(
3117         {
3118             class => 'Koha::Patrons',
3119             value => { categorycode => $patron_category->{categorycode} }
3120         }
3121     )->store;
3122
3123     my $itemtype = $builder->build_object(
3124         {
3125             class => 'Koha::ItemTypes',
3126             value  => {
3127                 notforloan          => undef,
3128                 rentalcharge        => 0,
3129                 rentalcharge_daily => 1.000000
3130             }
3131         }
3132     )->store;
3133
3134     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
3135     my $item = $builder->build_object(
3136         {
3137             class => 'Koha::Items',
3138             value => {
3139                 homebranch       => $library->id,
3140                 holdingbranch    => $library->id,
3141                 notforloan       => 0,
3142                 itemlost         => 0,
3143                 withdrawn        => 0,
3144                 itype            => $itemtype->id,
3145                 biblionumber     => $biblioitem->{biblionumber},
3146                 biblioitemnumber => $biblioitem->{biblioitemnumber},
3147             }
3148         }
3149     )->store;
3150
3151     is( $itemtype->rentalcharge_daily, '1.000000', 'Daily rental charge stored and retreived correctly' );
3152     is( $item->effective_itemtype, $itemtype->id, "Itemtype set correctly for item");
3153
3154     my $dt_from = dt_from_string();
3155     my $dt_to = dt_from_string()->add( days => 7 );
3156     my $dt_to_renew = dt_from_string()->add( days => 13 );
3157
3158     t::lib::Mocks::mock_preference('finesCalendar', 'ignoreCalendar');
3159     my $issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3160     my $accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
3161     is( $accountline->amount, '7.000000', "Daily rental charge calculated correctly with finesCalendar = ignoreCalendar" );
3162     $accountline->delete();
3163     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3164     $accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
3165     is( $accountline->amount, '6.000000', "Daily rental charge calculated correctly with finesCalendar = ignoreCalendar, for renewal" );
3166     $accountline->delete();
3167     $issue->delete();
3168
3169     t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
3170     $issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3171     $accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
3172     is( $accountline->amount, '7.000000', "Daily rental charge calculated correctly with finesCalendar = noFinesWhenClosed" );
3173     $accountline->delete();
3174     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3175     $accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
3176     is( $accountline->amount, '6.000000', "Daily rental charge calculated correctly with finesCalendar = noFinesWhenClosed, for renewal" );
3177     $accountline->delete();
3178     $issue->delete();
3179
3180     my $calendar = C4::Calendar->new( branchcode => $library->id );
3181     $calendar->insert_week_day_holiday(
3182         weekday     => 3,
3183         title       => 'Test holiday',
3184         description => 'Test holiday'
3185     );
3186     $issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
3187     $accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
3188     is( $accountline->amount, '6.000000', "Daily rental charge calculated correctly with finesCalendar = noFinesWhenClosed and closed Wednesdays" );
3189     $accountline->delete();
3190     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3191     $accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
3192     is( $accountline->amount, '5.000000', "Daily rental charge calculated correctly with finesCalendar = noFinesWhenClosed and closed Wednesdays, for renewal" );
3193     $accountline->delete();
3194     $issue->delete();
3195
3196     $itemtype->rentalcharge('2.000000')->store;
3197     is( $itemtype->rentalcharge, '2.000000', 'Rental charge updated and retreived correctly' );
3198     $issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from);
3199     my $accountlines = Koha::Account::Lines->search({ itemnumber => $item->id });
3200     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly");
3201     $accountlines->delete();
3202     AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
3203     $accountlines = Koha::Account::Lines->search({ itemnumber => $item->id });
3204     is( $accountlines->count, '2', "Fixed charge and accrued charge recorded distinctly, for renewal");
3205     $accountlines->delete();
3206     $issue->delete();
3207 };
3208
3209 subtest 'CanBookBeIssued & RentalFeesCheckoutConfirmation' => sub {
3210     plan tests => 2;
3211
3212     t::lib::Mocks::mock_preference('RentalFeesCheckoutConfirmation', 1);
3213     t::lib::Mocks::mock_preference('item-level_itypes', 1);
3214
3215     my $library =
3216       $builder->build_object( { class => 'Koha::Libraries' } )->store;
3217     my $patron = $builder->build_object(
3218         {
3219             class => 'Koha::Patrons',
3220             value => { categorycode => $patron_category->{categorycode} }
3221         }
3222     )->store;
3223
3224     my $itemtype = $builder->build_object(
3225         {
3226             class => 'Koha::ItemTypes',
3227             value => {
3228                 notforloan             => 0,
3229                 rentalcharge           => 0,
3230                 rentalcharge_daily => 0
3231             }
3232         }
3233     );
3234
3235     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
3236     my $item = $builder->build_object(
3237         {
3238             class => 'Koha::Items',
3239             value  => {
3240                 homebranch    => $library->id,
3241                 holdingbranch => $library->id,
3242                 notforloan    => 0,
3243                 itemlost      => 0,
3244                 withdrawn     => 0,
3245                 itype         => $itemtype->id,
3246                 biblionumber  => $biblioitem->{biblionumber},
3247                 biblioitemnumber => $biblioitem->{biblioitemnumber},
3248             }
3249         }
3250     )->store;
3251
3252     my ( $issuingimpossible, $needsconfirmation );
3253     my $dt_from = dt_from_string();
3254     my $dt_due = dt_from_string()->add( days => 3 );
3255
3256     $itemtype->rentalcharge('1.000000')->store;
3257     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
3258     is_deeply( $needsconfirmation, { RENTALCHARGE => '1' }, 'Item needs rentalcharge confirmation to be issued' );
3259     $itemtype->rentalcharge('0')->store;
3260     $itemtype->rentalcharge_daily('1.000000')->store;
3261     ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $item->barcode, $dt_due, undef, undef, undef );
3262     is_deeply( $needsconfirmation, { RENTALCHARGE => '3' }, 'Item needs rentalcharge confirmation to be issued, increment' );
3263     $itemtype->rentalcharge_daily('0')->store;
3264 };