Bug 14146: Add tests for AddReturn + CumulativeRestrictionPeriods
[koha.git] / t / db_dependent / Circulation.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Test::More tests => 92;
21
22 BEGIN {
23     require_ok('C4::Circulation');
24 }
25
26 use DateTime;
27
28 use t::lib::Mocks;
29 use t::lib::TestBuilder;
30
31 use C4::Circulation;
32 use C4::Biblio;
33 use C4::Items;
34 use C4::Members;
35 use C4::Reserves;
36 use C4::Overdues qw(UpdateFine CalcFine);
37 use Koha::DateUtils;
38 use Koha::Database;
39 use Koha::IssuingRules;
40 use Koha::Subscriptions;
41
42 my $schema = Koha::Database->schema;
43 $schema->storage->txn_begin;
44 my $builder = t::lib::TestBuilder->new;
45 my $dbh = C4::Context->dbh;
46
47 # Start transaction
48 $dbh->{RaiseError} = 1;
49
50 # Start with a clean slate
51 $dbh->do('DELETE FROM issues');
52
53 my $library = $builder->build({
54     source => 'Branch',
55 });
56 my $library2 = $builder->build({
57     source => 'Branch',
58 });
59 my $itemtype = $builder->build(
60     {   source => 'Itemtype',
61         value  => { notforloan => undef, rentalcharge => 0 }
62     }
63 )->{itemtype};
64
65 my $CircControl = C4::Context->preference('CircControl');
66 my $HomeOrHoldingBranch = C4::Context->preference('HomeOrHoldingBranch');
67
68 my $item = {
69     homebranch => $library2->{branchcode},
70     holdingbranch => $library2->{branchcode}
71 };
72
73 my $borrower = {
74     branchcode => $library2->{branchcode}
75 };
76
77 # No userenv, PickupLibrary
78 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
79 is(
80     C4::Context->preference('CircControl'),
81     'PickupLibrary',
82     'CircControl changed to PickupLibrary'
83 );
84 is(
85     C4::Circulation::_GetCircControlBranch($item, $borrower),
86     $item->{$HomeOrHoldingBranch},
87     '_GetCircControlBranch returned item branch (no userenv defined)'
88 );
89
90 # No userenv, PatronLibrary
91 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
92 is(
93     C4::Context->preference('CircControl'),
94     'PatronLibrary',
95     'CircControl changed to PatronLibrary'
96 );
97 is(
98     C4::Circulation::_GetCircControlBranch($item, $borrower),
99     $borrower->{branchcode},
100     '_GetCircControlBranch returned borrower branch'
101 );
102
103 # No userenv, ItemHomeLibrary
104 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
105 is(
106     C4::Context->preference('CircControl'),
107     'ItemHomeLibrary',
108     'CircControl changed to ItemHomeLibrary'
109 );
110 is(
111     $item->{$HomeOrHoldingBranch},
112     C4::Circulation::_GetCircControlBranch($item, $borrower),
113     '_GetCircControlBranch returned item branch'
114 );
115
116 # Now, set a userenv
117 C4::Context->_new_userenv('xxx');
118 C4::Context->set_userenv(0,0,0,'firstname','surname', $library2->{branchcode}, 'Midway Public Library', '', '', '');
119 is(C4::Context->userenv->{branch}, $library2->{branchcode}, 'userenv set');
120
121 # Userenv set, PickupLibrary
122 t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary');
123 is(
124     C4::Context->preference('CircControl'),
125     'PickupLibrary',
126     'CircControl changed to PickupLibrary'
127 );
128 is(
129     C4::Circulation::_GetCircControlBranch($item, $borrower),
130     $library2->{branchcode},
131     '_GetCircControlBranch returned current branch'
132 );
133
134 # Userenv set, PatronLibrary
135 t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary');
136 is(
137     C4::Context->preference('CircControl'),
138     'PatronLibrary',
139     'CircControl changed to PatronLibrary'
140 );
141 is(
142     C4::Circulation::_GetCircControlBranch($item, $borrower),
143     $borrower->{branchcode},
144     '_GetCircControlBranch returned borrower branch'
145 );
146
147 # Userenv set, ItemHomeLibrary
148 t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary');
149 is(
150     C4::Context->preference('CircControl'),
151     'ItemHomeLibrary',
152     'CircControl changed to ItemHomeLibrary'
153 );
154 is(
155     C4::Circulation::_GetCircControlBranch($item, $borrower),
156     $item->{$HomeOrHoldingBranch},
157     '_GetCircControlBranch returned item branch'
158 );
159
160 # Reset initial configuration
161 t::lib::Mocks::mock_preference('CircControl', $CircControl);
162 is(
163     C4::Context->preference('CircControl'),
164     $CircControl,
165     'CircControl reset to its initial value'
166 );
167
168 # Set a simple circ policy
169 $dbh->do('DELETE FROM issuingrules');
170 $dbh->do(
171     q{INSERT INTO issuingrules (categorycode, branchcode, itemtype, reservesallowed,
172                                 maxissueqty, issuelength, lengthunit,
173                                 renewalsallowed, renewalperiod,
174                                 norenewalbefore, auto_renew,
175                                 fine, chargeperiod)
176       VALUES (?, ?, ?, ?,
177               ?, ?, ?,
178               ?, ?,
179               ?, ?,
180               ?, ?
181              )
182     },
183     {},
184     '*', '*', '*', 25,
185     20, 14, 'days',
186     1, 7,
187     undef, 0,
188     .10, 1
189 );
190
191 # Test C4::Circulation::ProcessOfflinePayment
192 my $sth = C4::Context->dbh->prepare("SELECT COUNT(*) FROM accountlines WHERE amount = '-123.45' AND accounttype = 'Pay'");
193 $sth->execute();
194 my ( $original_count ) = $sth->fetchrow_array();
195
196 C4::Context->dbh->do("INSERT INTO borrowers ( cardnumber, surname, firstname, categorycode, branchcode ) VALUES ( '99999999999', 'Hall', 'Kyle', 'S', ? )", undef, $library2->{branchcode} );
197
198 C4::Circulation::ProcessOfflinePayment({ cardnumber => '99999999999', amount => '123.45' });
199
200 $sth->execute();
201 my ( $new_count ) = $sth->fetchrow_array();
202
203 ok( $new_count == $original_count  + 1, 'ProcessOfflinePayment makes payment correctly' );
204
205 C4::Context->dbh->do("DELETE FROM accountlines WHERE borrowernumber IN ( SELECT borrowernumber FROM borrowers WHERE cardnumber = '99999999999' )");
206 C4::Context->dbh->do("DELETE FROM borrowers WHERE cardnumber = '99999999999'");
207 C4::Context->dbh->do("DELETE FROM accountlines");
208 {
209 # CanBookBeRenewed tests
210
211     # Generate test biblio
212     my $biblio = MARC::Record->new();
213     my $title = 'Silence in the library';
214     $biblio->append_fields(
215         MARC::Field->new('100', ' ', ' ', a => 'Moffat, Steven'),
216         MARC::Field->new('245', ' ', ' ', a => $title),
217     );
218
219     my ($biblionumber, $biblioitemnumber) = AddBiblio($biblio, '');
220
221     my $barcode = 'R00000342';
222     my $branch = $library2->{branchcode};
223
224     my ( $item_bibnum, $item_bibitemnum, $itemnumber ) = AddItem(
225         {
226             homebranch       => $branch,
227             holdingbranch    => $branch,
228             barcode          => $barcode,
229             replacementprice => 12.00,
230             itype            => $itemtype
231         },
232         $biblionumber
233     );
234
235     my $barcode2 = 'R00000343';
236     my ( $item_bibnum2, $item_bibitemnum2, $itemnumber2 ) = AddItem(
237         {
238             homebranch       => $branch,
239             holdingbranch    => $branch,
240             barcode          => $barcode2,
241             replacementprice => 23.00,
242             itype            => $itemtype
243         },
244         $biblionumber
245     );
246
247     my $barcode3 = 'R00000346';
248     my ( $item_bibnum3, $item_bibitemnum3, $itemnumber3 ) = AddItem(
249         {
250             homebranch       => $branch,
251             holdingbranch    => $branch,
252             barcode          => $barcode3,
253             replacementprice => 23.00,
254             itype            => $itemtype
255         },
256         $biblionumber
257     );
258
259
260
261
262     # Create borrowers
263     my %renewing_borrower_data = (
264         firstname =>  'John',
265         surname => 'Renewal',
266         categorycode => 'S',
267         branchcode => $branch,
268     );
269
270     my %reserving_borrower_data = (
271         firstname =>  'Katrin',
272         surname => 'Reservation',
273         categorycode => 'S',
274         branchcode => $branch,
275     );
276
277     my %hold_waiting_borrower_data = (
278         firstname =>  'Kyle',
279         surname => 'Reservation',
280         categorycode => 'S',
281         branchcode => $branch,
282     );
283
284     my %restricted_borrower_data = (
285         firstname =>  'Alice',
286         surname => 'Reservation',
287         categorycode => 'S',
288         debarred => '3228-01-01',
289         branchcode => $branch,
290     );
291
292     my $renewing_borrowernumber = AddMember(%renewing_borrower_data);
293     my $reserving_borrowernumber = AddMember(%reserving_borrower_data);
294     my $hold_waiting_borrowernumber = AddMember(%hold_waiting_borrower_data);
295     my $restricted_borrowernumber = AddMember(%restricted_borrower_data);
296
297     my $renewing_borrower = GetMember( borrowernumber => $renewing_borrowernumber );
298     my $restricted_borrower = GetMember( borrowernumber => $restricted_borrowernumber );
299
300     my $bibitems       = '';
301     my $priority       = '1';
302     my $resdate        = undef;
303     my $expdate        = undef;
304     my $notes          = '';
305     my $checkitem      = undef;
306     my $found          = undef;
307
308     my $issue = AddIssue( $renewing_borrower, $barcode);
309     my $datedue = dt_from_string( $issue->date_due() );
310     is (defined $issue->date_due(), 1, "Item 1 checked out, due date: " . $issue->date_due() );
311
312     my $issue2 = AddIssue( $renewing_borrower, $barcode2);
313     $datedue = dt_from_string( $issue->date_due() );
314     is (defined $issue2, 1, "Item 2 checked out, due date: " . $issue2->date_due());
315
316
317     my $borrowing_borrowernumber = GetItemIssue($itemnumber)->{borrowernumber};
318     is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}");
319
320     my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber, 1);
321     is( $renewokay, 1, 'Can renew, no holds for this title or item');
322
323
324     # Biblio-level hold, renewal test
325     AddReserve(
326         $branch, $reserving_borrowernumber, $biblionumber,
327         $bibitems,  $priority, $resdate, $expdate, $notes,
328         $title, $checkitem, $found
329     );
330
331     # Testing of feature to allow the renewal of reserved items if other items on the record can fill all needed holds
332     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
333     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 1 );
334     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber);
335     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
336     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber2);
337     is( $renewokay, 1, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
338
339     # Now let's add an item level hold, we should no longer be able to renew the item
340     my $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
341         {
342             borrowernumber => $hold_waiting_borrowernumber,
343             biblionumber   => $biblionumber,
344             itemnumber     => $itemnumber,
345             branchcode     => $branch,
346             priority       => 3,
347         }
348     );
349     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber);
350     is( $renewokay, 0, 'Bug 13919 - Renewal possible with item level hold on item');
351     $hold->delete();
352
353     # 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
354     # be able to renew these items
355     $hold = Koha::Database->new()->schema()->resultset('Reserve')->create(
356         {
357             borrowernumber => $hold_waiting_borrowernumber,
358             biblionumber   => $biblionumber,
359             itemnumber     => $itemnumber3,
360             branchcode     => $branch,
361             priority       => 0,
362             found          => 'W'
363         }
364     );
365     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber);
366     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
367     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber2);
368     is( $renewokay, 0, 'Bug 11634 - Allow renewal of item with unfilled holds if other available items can fill those holds');
369     t::lib::Mocks::mock_preference('AllowRenewalIfOtherItemsAvailable', 0 );
370
371     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber);
372     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
373     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
374
375     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber2);
376     is( $renewokay, 0, '(Bug 10663) Cannot renew, reserved');
377     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, reserved (returned error is on_reserve)');
378
379     my $reserveid = C4::Reserves::GetReserveId({ biblionumber => $biblionumber, borrowernumber => $reserving_borrowernumber});
380     my $reserving_borrower = GetMember( borrowernumber => $reserving_borrowernumber );
381     AddIssue($reserving_borrower, $barcode3);
382     my $reserve = $dbh->selectrow_hashref(
383         'SELECT * FROM old_reserves WHERE reserve_id = ?',
384         { Slice => {} },
385         $reserveid
386     );
387     is($reserve->{found}, 'F', 'hold marked completed when checking out item that fills it');
388
389     # Item-level hold, renewal test
390     AddReserve(
391         $branch, $reserving_borrowernumber, $biblionumber,
392         $bibitems,  $priority, $resdate, $expdate, $notes,
393         $title, $itemnumber, $found
394     );
395
396     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber, 1);
397     is( $renewokay, 0, '(Bug 10663) Cannot renew, item reserved');
398     is( $error, 'on_reserve', '(Bug 10663) Cannot renew, item reserved (returned error is on_reserve)');
399
400     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber2, 1);
401     is( $renewokay, 1, 'Can renew item 2, item-level hold is on item 1');
402
403     # Items can't fill hold for reasons
404     ModItem({ notforloan => 1 }, $biblionumber, $itemnumber);
405     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber, 1);
406     is( $renewokay, 1, 'Can renew, item is marked not for loan, hold does not block');
407     ModItem({ notforloan => 0, itype => $itemtype }, $biblionumber, $itemnumber,1);
408
409     # FIXME: Add more for itemtype not for loan etc.
410
411     # Restricted users cannot renew when RestrictionBlockRenewing is enabled
412     my $barcode5 = 'R00000347';
413     my ( $item_bibnum5, $item_bibitemnum5, $itemnumber5 ) = AddItem(
414         {
415             homebranch       => $branch,
416             holdingbranch    => $branch,
417             barcode          => $barcode5,
418             replacementprice => 23.00,
419             itype            => $itemtype
420         },
421         $biblionumber
422     );
423     my $datedue5 = AddIssue($restricted_borrower, $barcode5);
424     is (defined $datedue5, 1, "Item with date due checked out, due date: $datedue5");
425
426     t::lib::Mocks::mock_preference('RestrictionBlockRenewing','1');
427     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber2);
428     is( $renewokay, 1, '(Bug 8236), Can renew, user is not restricted');
429     ( $renewokay, $error ) = CanBookBeRenewed($restricted_borrowernumber, $itemnumber5);
430     is( $renewokay, 0, '(Bug 8236), Cannot renew, user is restricted');
431
432     # Users cannot renew an overdue item
433     my $barcode6 = 'R00000348';
434     my ( $item_bibnum6, $item_bibitemnum6, $itemnumber6 ) = AddItem(
435         {
436             homebranch       => $branch,
437             holdingbranch    => $branch,
438             barcode          => $barcode6,
439             replacementprice => 23.00,
440             itype            => $itemtype
441         },
442         $biblionumber
443     );
444
445     my $barcode7 = 'R00000349';
446     my ( $item_bibnum7, $item_bibitemnum7, $itemnumber7 ) = AddItem(
447         {
448             homebranch       => $branch,
449             holdingbranch    => $branch,
450             barcode          => $barcode7,
451             replacementprice => 23.00,
452             itype            => $itemtype
453         },
454         $biblionumber
455     );
456     my $datedue6 = AddIssue( $renewing_borrower, $barcode6);
457     is (defined $datedue6, 1, "Item 2 checked out, due date: $datedue6");
458
459     my $now = dt_from_string();
460     my $five_weeks = DateTime::Duration->new(weeks => 5);
461     my $five_weeks_ago = $now - $five_weeks;
462
463     my $passeddatedue1 = AddIssue($renewing_borrower, $barcode7, $five_weeks_ago);
464     is (defined $passeddatedue1, 1, "Item with passed date due checked out, due date: " . $passeddatedue1->date_due);
465
466     my ( $fine ) = CalcFine( GetItem(undef, $barcode7), $renewing_borrower->{categorycode}, $branch, $five_weeks_ago, $now );
467     C4::Overdues::UpdateFine(
468         {
469             issue_id       => $passeddatedue1->id(),
470             itemnumber     => $itemnumber7,
471             borrowernumber => $renewing_borrower->{borrowernumber},
472             amount         => $fine,
473             type           => 'FU',
474             due            => Koha::DateUtils::output_pref($five_weeks_ago)
475         }
476     );
477     AddRenewal( $renewing_borrower->{borrowernumber}, $itemnumber7, $branch );
478     $fine = $schema->resultset('Accountline')->single( { borrowernumber => $renewing_borrower->{borrowernumber}, itemnumber => $itemnumber7 } );
479     is( $fine->accounttype, 'F', 'Fine on renewed item is closed out properly' );
480     $fine->delete();
481
482     t::lib::Mocks::mock_preference('OverduesBlockRenewing','blockitem');
483     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber6);
484     is( $renewokay, 1, '(Bug 8236), Can renew, this item is not overdue');
485     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber7);
486     is( $renewokay, 0, '(Bug 8236), Cannot renew, this item is overdue');
487
488
489     $reserveid = C4::Reserves::GetReserveId({ biblionumber => $biblionumber, itemnumber => $itemnumber, borrowernumber => $reserving_borrowernumber});
490     CancelReserve({ reserve_id => $reserveid });
491
492     # Bug 14101
493     # Test automatic renewal before value for "norenewalbefore" in policy is set
494     # In this case automatic renewal is not permitted prior to due date
495     my $barcode4 = '11235813';
496     my ( $item_bibnum4, $item_bibitemnum4, $itemnumber4 ) = AddItem(
497         {
498             homebranch       => $branch,
499             holdingbranch    => $branch,
500             barcode          => $barcode4,
501             replacementprice => 16.00,
502             itype            => $itemtype
503         },
504         $biblionumber
505     );
506
507     $issue = AddIssue( $renewing_borrower, $barcode4, undef, undef, undef, undef, { auto_renew => 1 } );
508     ( $renewokay, $error ) =
509       CanBookBeRenewed( $renewing_borrowernumber, $itemnumber4 );
510     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
511     is( $error, 'auto_too_soon',
512         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = undef (returned code is auto_too_soon)' );
513
514     # Bug 7413
515     # Test premature manual renewal
516     $dbh->do('UPDATE issuingrules SET norenewalbefore = 7');
517
518     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber);
519     is( $renewokay, 0, 'Bug 7413: Cannot renew, renewal is premature');
520     is( $error, 'too_soon', 'Bug 7413: Cannot renew, renewal is premature (returned code is too_soon)');
521
522     # Bug 14395
523     # Test 'exact time' setting for syspref NoRenewalBeforePrecision
524     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'exact_time' );
525     is(
526         GetSoonestRenewDate( $renewing_borrowernumber, $itemnumber ),
527         $datedue->clone->add( days => -7 ),
528         'Bug 14395: Renewals permitted 7 days before due date, as expected'
529     );
530
531     # Bug 14395
532     # Test 'date' setting for syspref NoRenewalBeforePrecision
533     t::lib::Mocks::mock_preference( 'NoRenewalBeforePrecision', 'date' );
534     is(
535         GetSoonestRenewDate( $renewing_borrowernumber, $itemnumber ),
536         $datedue->clone->add( days => -7 )->truncate( to => 'day' ),
537         'Bug 14395: Renewals permitted 7 days before due date, as expected'
538     );
539
540     # Bug 14101
541     # Test premature automatic renewal
542     ( $renewokay, $error ) =
543       CanBookBeRenewed( $renewing_borrowernumber, $itemnumber4 );
544     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
545     is( $error, 'auto_too_soon',
546         'Bug 14101: Cannot renew, renewal is automatic and premature (returned code is auto_too_soon)'
547     );
548
549     # Change policy so that loans can only be renewed exactly on due date (0 days prior to due date)
550     # and test automatic renewal again
551     $dbh->do('UPDATE issuingrules SET norenewalbefore = 0');
552     ( $renewokay, $error ) =
553       CanBookBeRenewed( $renewing_borrowernumber, $itemnumber4 );
554     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic and premature' );
555     is( $error, 'auto_too_soon',
556         'Bug 14101: Cannot renew, renewal is automatic and premature, "No renewal before" = 0 (returned code is auto_too_soon)'
557     );
558
559     # Change policy so that loans can be renewed 99 days prior to the due date
560     # and test automatic renewal again
561     $dbh->do('UPDATE issuingrules SET norenewalbefore = 99');
562     ( $renewokay, $error ) =
563       CanBookBeRenewed( $renewing_borrowernumber, $itemnumber4 );
564     is( $renewokay, 0, 'Bug 14101: Cannot renew, renewal is automatic' );
565     is( $error, 'auto_renew',
566         'Bug 14101: Cannot renew, renewal is automatic (returned code is auto_renew)'
567     );
568
569     subtest "too_late_renewal / no_auto_renewal_after" => sub {
570         plan tests => 8;
571         my $item_to_auto_renew = $builder->build(
572             {   source => 'Item',
573                 value  => {
574                     biblionumber  => $biblionumber,
575                     homebranch    => $branch,
576                     holdingbranch => $branch,
577                 }
578             }
579         );
580
581         my $ten_days_before = dt_from_string->add( days => -10 );
582         my $ten_days_ahead  = dt_from_string->add( days => 10 );
583         AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
584
585         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = 9');
586         ( $renewokay, $error ) =
587           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
588         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
589         is( $error, 'auto_too_late', 'Cannot renew, too late(returned code is auto_too_late)' );
590
591         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = 10');
592         ( $renewokay, $error ) =
593           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
594         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
595         is( $error, 'auto_too_late', 'Cannot auto renew, too late - no_auto_renewal_after is inclusive(returned code is auto_too_late)' );
596
597         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = 11');
598         ( $renewokay, $error ) =
599           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
600         is( $renewokay, 0, 'Do not renew, renewal is automatic' );
601         is( $error, 'auto_too_soon', 'Cannot auto renew, too soon - no_auto_renewal_after is defined(returned code is auto_too_soon)' );
602
603         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 11');
604         ( $renewokay, $error ) =
605           CanBookBeRenewed( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
606         is( $renewokay, 0,            'Do not renew, renewal is automatic' );
607         is( $error,     'auto_renew', 'Cannot renew, renew is automatic' );
608     };
609
610     subtest "GetLatestAutoRenewDate" => sub {
611         plan tests => 3;
612         my $item_to_auto_renew = $builder->build(
613             {   source => 'Item',
614                 value  => {
615                     biblionumber  => $biblionumber,
616                     homebranch    => $branch,
617                     holdingbranch => $branch,
618                 }
619             }
620         );
621
622         my $ten_days_before = dt_from_string->add( days => -10 );
623         my $ten_days_ahead  = dt_from_string->add( days => 10 );
624         AddIssue( $renewing_borrower, $item_to_auto_renew->{barcode}, $ten_days_ahead, undef, $ten_days_before, undef, { auto_renew => 1 } );
625         $dbh->do('UPDATE issuingrules SET norenewalbefore = 7, no_auto_renewal_after = ""');
626         my $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
627         is( $latest_auto_renew_date, undef, 'GetLatestAutoRenewDate should return undef if no_auto_renewal_after is not defined' );
628         my $five_days_before = dt_from_string->add( days => -5 );
629         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 5');
630         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
631         is( $latest_auto_renew_date->truncate( to => 'minute' ),
632             $five_days_before->truncate( to => 'minute' ),
633             'GetLatestAutoRenewDate should return -5 days if no_auto_renewal_after = 5 and date_due is 10 days before'
634         );
635         my $five_days_ahead = dt_from_string->add( days => 5 );
636         $dbh->do('UPDATE issuingrules SET norenewalbefore = 10, no_auto_renewal_after = 15');
637         $latest_auto_renew_date = GetLatestAutoRenewDate( $renewing_borrowernumber, $item_to_auto_renew->{itemnumber} );
638         is( $latest_auto_renew_date->truncate( to => 'minute' ),
639             $five_days_ahead->truncate( to => 'minute' ),
640             'GetLatestAutoRenewDate should return +5 days if no_auto_renewal_after = 15 and date_due is 10 days before'
641         );
642     };
643
644     # Too many renewals
645
646     # set policy to forbid renewals
647     $dbh->do('UPDATE issuingrules SET norenewalbefore = NULL, renewalsallowed = 0');
648
649     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber);
650     is( $renewokay, 0, 'Cannot renew, 0 renewals allowed');
651     is( $error, 'too_many', 'Cannot renew, 0 renewals allowed (returned code is too_many)');
652
653     # Test WhenLostForgiveFine and WhenLostChargeReplacementFee
654     t::lib::Mocks::mock_preference('WhenLostForgiveFine','1');
655     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','1');
656
657     C4::Overdues::UpdateFine(
658         {
659             issue_id       => $issue->id(),
660             itemnumber     => $itemnumber,
661             borrowernumber => $renewing_borrower->{borrowernumber},
662             amount         => 15.00,
663             type           => q{},
664             due            => Koha::DateUtils::output_pref($datedue)
665         }
666     );
667
668     LostItem( $itemnumber, 1 );
669
670     my $item = Koha::Database->new()->schema()->resultset('Item')->find($itemnumber);
671     ok( !$item->onloan(), "Lost item marked as returned has false onloan value" );
672
673     my $total_due = $dbh->selectrow_array(
674         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
675         undef, $renewing_borrower->{borrowernumber}
676     );
677
678     ok( $total_due == 12, 'Borrower only charged replacement fee with both WhenLostForgiveFine and WhenLostChargeReplacementFee enabled' );
679
680     C4::Context->dbh->do("DELETE FROM accountlines");
681
682     t::lib::Mocks::mock_preference('WhenLostForgiveFine','0');
683     t::lib::Mocks::mock_preference('WhenLostChargeReplacementFee','0');
684
685     C4::Overdues::UpdateFine(
686         {
687             issue_id       => $issue2->id(),
688             itemnumber     => $itemnumber2,
689             borrowernumber => $renewing_borrower->{borrowernumber},
690             amount         => 15.00,
691             type           => q{},
692             due            => Koha::DateUtils::output_pref($datedue)
693         }
694     );
695
696     LostItem( $itemnumber2, 0 );
697
698     my $item2 = Koha::Database->new()->schema()->resultset('Item')->find($itemnumber2);
699     ok( $item2->onloan(), "Lost item *not* marked as returned has true onloan value" );
700
701     $total_due = $dbh->selectrow_array(
702         'SELECT SUM( amountoutstanding ) FROM accountlines WHERE borrowernumber = ?',
703         undef, $renewing_borrower->{borrowernumber}
704     );
705
706     ok( $total_due == 15, 'Borrower only charged fine with both WhenLostForgiveFine and WhenLostChargeReplacementFee disabled' );
707
708     my $future = dt_from_string();
709     $future->add( days => 7 );
710     my $units = C4::Overdues::get_chargeable_units('days', $future, $now, $library2->{branchcode});
711     ok( $units == 0, '_get_chargeable_units returns 0 for items not past due date (Bug 12596)' );
712
713     # Users cannot renew any item if there is an overdue item
714     t::lib::Mocks::mock_preference('OverduesBlockRenewing','block');
715     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber6);
716     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
717     ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber7);
718     is( $renewokay, 0, '(Bug 8236), Cannot renew, one of the items is overdue');
719
720   }
721
722 {
723     # GetUpcomingDueIssues tests
724     my $barcode  = 'R00000342';
725     my $barcode2 = 'R00000343';
726     my $barcode3 = 'R00000344';
727     my $branch   = $library2->{branchcode};
728
729     #Create another record
730     my $biblio2 = MARC::Record->new();
731     my $title2 = 'Something is worng here';
732     $biblio2->append_fields(
733         MARC::Field->new('100', ' ', ' ', a => 'Anonymous'),
734         MARC::Field->new('245', ' ', ' ', a => $title2),
735     );
736     my ($biblionumber2, $biblioitemnumber2) = AddBiblio($biblio2, '');
737
738     #Create third item
739     AddItem(
740         {
741             homebranch       => $branch,
742             holdingbranch    => $branch,
743             barcode          => $barcode3,
744             itype            => $itemtype
745         },
746         $biblionumber2
747     );
748
749     # Create a borrower
750     my %a_borrower_data = (
751         firstname =>  'Fridolyn',
752         surname => 'SOMERS',
753         categorycode => 'S',
754         branchcode => $branch,
755     );
756
757     my $a_borrower_borrowernumber = AddMember(%a_borrower_data);
758     my $a_borrower = GetMember( borrowernumber => $a_borrower_borrowernumber );
759
760     my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 );
761     my $two_days_ahead = DateTime->today(time_zone => C4::Context->tz())->add( days => 2 );
762     my $today = DateTime->today(time_zone => C4::Context->tz());
763
764     my $issue = AddIssue( $a_borrower, $barcode, $yesterday );
765     my $datedue = dt_from_string( $issue->date_due() );
766     my $issue2 = AddIssue( $a_borrower, $barcode2, $two_days_ahead );
767     my $datedue2 = dt_from_string( $issue->date_due() );
768
769     my $upcoming_dues;
770
771     # GetUpcomingDueIssues tests
772     for my $i(0..1) {
773         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
774         is ( scalar( @$upcoming_dues ), 0, "No items due in less than one day ($i days in advance)" );
775     }
776
777     #days_in_advance needs to be inclusive, so 1 matches items due tomorrow, 0 items due today etc.
778     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 } );
779     is ( scalar ( @$upcoming_dues), 1, "Only one item due in 2 days or less" );
780
781     for my $i(3..5) {
782         $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $i } );
783         is ( scalar( @$upcoming_dues ), 1,
784             "Bug 9362: Only one item due in more than 2 days ($i days in advance)" );
785     }
786
787     # Bug 11218 - Due notices not generated - GetUpcomingDueIssues needs to select due today items as well
788
789     my $issue3 = AddIssue( $a_borrower, $barcode3, $today );
790
791     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => -1 } );
792     is ( scalar ( @$upcoming_dues), 0, "Overdues can not be selected" );
793
794     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 0 } );
795     is ( scalar ( @$upcoming_dues), 1, "1 item is due today" );
796
797     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 1 } );
798     is ( scalar ( @$upcoming_dues), 1, "1 item is due today, none tomorrow" );
799
800     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 2 }  );
801     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
802
803     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => 3 } );
804     is ( scalar ( @$upcoming_dues), 2, "2 items are due withing 2 days" );
805
806     $upcoming_dues = C4::Circulation::GetUpcomingDueIssues();
807     is ( scalar ( @$upcoming_dues), 2, "days_in_advance is 7 in GetUpcomingDueIssues if not provided" );
808
809 }
810
811 {
812     my $barcode  = '1234567890';
813     my $branch   = $library2->{branchcode};
814
815     my $biblio = MARC::Record->new();
816     my ($biblionumber, $biblioitemnumber) = AddBiblio($biblio, '');
817
818     #Create third item
819     my ( undef, undef, $itemnumber ) = AddItem(
820         {
821             homebranch       => $branch,
822             holdingbranch    => $branch,
823             barcode          => $barcode,
824             itype            => $itemtype
825         },
826         $biblionumber
827     );
828
829     # Create a borrower
830     my %a_borrower_data = (
831         firstname =>  'Kyle',
832         surname => 'Hall',
833         categorycode => 'S',
834         branchcode => $branch,
835     );
836
837     my $borrowernumber = AddMember(%a_borrower_data);
838
839     my $issue = AddIssue( GetMember( borrowernumber => $borrowernumber ), $barcode );
840     UpdateFine(
841         {
842             issue_id       => $issue->id(),
843             itemnumber     => $itemnumber,
844             borrowernumber => $borrowernumber,
845             amount         => 0,
846             type           => q{}
847         }
848     );
849
850     my $hr = $dbh->selectrow_hashref(q{SELECT COUNT(*) AS count FROM accountlines WHERE borrowernumber = ? AND itemnumber = ?}, undef, $borrowernumber, $itemnumber );
851     my $count = $hr->{count};
852
853     is ( $count, 0, "Calling UpdateFine on non-existant fine with an amount of 0 does not result in an empty fine" );
854 }
855
856 {
857     $dbh->do('DELETE FROM issues');
858     $dbh->do('DELETE FROM items');
859     $dbh->do('DELETE FROM issuingrules');
860     $dbh->do(
861         q{
862         INSERT INTO issuingrules ( categorycode, branchcode, itemtype, reservesallowed, maxissueqty, issuelength, lengthunit, renewalsallowed, renewalperiod,
863                     norenewalbefore, auto_renew, fine, chargeperiod ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
864         },
865         {},
866         '*', '*', '*', 25,
867         20,  14,  'days',
868         1,   7,
869         undef,  0,
870         .10, 1
871     );
872     my $biblio = MARC::Record->new();
873     my ( $biblionumber, $biblioitemnumber ) = AddBiblio( $biblio, '' );
874
875     my $barcode1 = '1234';
876     my ( undef, undef, $itemnumber1 ) = AddItem(
877         {
878             homebranch    => $library2->{branchcode},
879             holdingbranch => $library2->{branchcode},
880             barcode       => $barcode1,
881             itype         => $itemtype
882         },
883         $biblionumber
884     );
885     my $barcode2 = '4321';
886     my ( undef, undef, $itemnumber2 ) = AddItem(
887         {
888             homebranch    => $library2->{branchcode},
889             holdingbranch => $library2->{branchcode},
890             barcode       => $barcode2,
891             itype         => $itemtype
892         },
893         $biblionumber
894     );
895
896     my $borrowernumber1 = AddMember(
897         firstname    => 'Kyle',
898         surname      => 'Hall',
899         categorycode => 'S',
900         branchcode   => $library2->{branchcode},
901     );
902     my $borrowernumber2 = AddMember(
903         firstname    => 'Chelsea',
904         surname      => 'Hall',
905         categorycode => 'S',
906         branchcode   => $library2->{branchcode},
907     );
908
909     my $borrower1 = GetMember( borrowernumber => $borrowernumber1 );
910     my $borrower2 = GetMember( borrowernumber => $borrowernumber2 );
911
912     my $issue = AddIssue( $borrower1, $barcode1 );
913
914     my ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $itemnumber1 );
915     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with no hold on the record' );
916
917     AddReserve(
918         $library2->{branchcode}, $borrowernumber2, $biblionumber,
919         '',  1, undef, undef, '',
920         undef, undef, undef
921     );
922
923     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 0");
924     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
925     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $itemnumber1 );
926     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfholds are disabled' );
927
928     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 0");
929     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
930     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $itemnumber1 );
931     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is enabled and onshelfholds is disabled' );
932
933     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
934     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 0 );
935     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $itemnumber1 );
936     is( $renewokay, 0, 'Bug 14337 - Verify the borrower cannot renew with a hold on the record if AllowRenewalIfOtherItemsAvailable is disabled and onshelfhold is enabled' );
937
938     C4::Context->dbh->do("UPDATE issuingrules SET onshelfholds = 1");
939     t::lib::Mocks::mock_preference( 'AllowRenewalIfOtherItemsAvailable', 1 );
940     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $itemnumber1 );
941     is( $renewokay, 1, 'Bug 14337 - Verify the borrower can renew with a hold on the record if AllowRenewalIfOtherItemsAvailable and onshelfhold are enabled' );
942
943     # Setting item not checked out to be not for loan but holdable
944     ModItem({ notforloan => -1 }, $biblionumber, $itemnumber2);
945
946     ( $renewokay, $error ) = CanBookBeRenewed( $borrowernumber1, $itemnumber1 );
947     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' );
948 }
949
950 {
951     # Don't allow renewing onsite checkout
952     my $barcode  = 'R00000XXX';
953     my $branch   = $library->{branchcode};
954
955     #Create another record
956     my $biblio = MARC::Record->new();
957     $biblio->append_fields(
958         MARC::Field->new('100', ' ', ' ', a => 'Anonymous'),
959         MARC::Field->new('245', ' ', ' ', a => 'A title'),
960     );
961     my ($biblionumber, $biblioitemnumber) = AddBiblio($biblio, '');
962
963     my (undef, undef, $itemnumber) = AddItem(
964         {
965             homebranch       => $branch,
966             holdingbranch    => $branch,
967             barcode          => $barcode,
968             itype            => $itemtype
969         },
970         $biblionumber
971     );
972
973     my $borrowernumber = AddMember(
974         firstname =>  'fn',
975         surname => 'dn',
976         categorycode => 'S',
977         branchcode => $branch,
978     );
979
980     my $borrower = GetMember( borrowernumber => $borrowernumber );
981     my $issue = AddIssue( $borrower, $barcode, undef, undef, undef, undef, { onsite_checkout => 1 } );
982     my ( $renewed, $error ) = CanBookBeRenewed( $borrowernumber, $itemnumber );
983     is( $renewed, 0, 'CanBookBeRenewed should not allow to renew on-site checkout' );
984     is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' );
985 }
986
987 {
988     my $library = $builder->build({ source => 'Branch' });
989
990     my $biblio = MARC::Record->new();
991     my ($biblionumber, $biblioitemnumber) = AddBiblio($biblio, '');
992
993     my $barcode = 'just a barcode';
994     my ( undef, undef, $itemnumber ) = AddItem(
995         {
996             homebranch       => $library->{branchcode},
997             holdingbranch    => $library->{branchcode},
998             barcode          => $barcode,
999             itype            => $itemtype
1000         },
1001         $biblionumber,
1002     );
1003
1004     my $patron = $builder->build({ source => 'Borrower', value => { branchcode => $library->{branchcode} } } );
1005
1006     my $issue = AddIssue( GetMember( borrowernumber => $patron->{borrowernumber} ), $barcode );
1007     UpdateFine(
1008         {
1009             issue_id       => $issue->id(),
1010             itemnumber     => $itemnumber,
1011             borrowernumber => $patron->{borrowernumber},
1012             amount         => 1,
1013             type           => q{}
1014         }
1015     );
1016     UpdateFine(
1017         {
1018             issue_id       => $issue->id(),
1019             itemnumber     => $itemnumber,
1020             borrowernumber => $patron->{borrowernumber},
1021             amount         => 2,
1022             type           => q{}
1023         }
1024     );
1025     is( Koha::Account::Lines->search({ issue_id => $issue->id })->count, 1, 'UpdateFine should not create a new accountline when updating an existing fine');
1026 }
1027
1028 subtest 'CanBookBeIssued & AllowReturnToBranch' => sub {
1029     plan tests => 23;
1030
1031     my $homebranch    = $builder->build( { source => 'Branch' } );
1032     my $holdingbranch = $builder->build( { source => 'Branch' } );
1033     my $otherbranch   = $builder->build( { source => 'Branch' } );
1034     my $patron_1      = $builder->build( { source => 'Borrower' } );
1035     my $patron_2      = $builder->build( { source => 'Borrower' } );
1036
1037     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1038     my $item = $builder->build(
1039         {   source => 'Item',
1040             value  => {
1041                 homebranch    => $homebranch->{branchcode},
1042                 holdingbranch => $holdingbranch->{branchcode},
1043                 notforloan    => 0,
1044                 itemlost      => 0,
1045                 withdrawn     => 0,
1046                 biblionumber  => $biblioitem->{biblionumber}
1047             }
1048         }
1049     );
1050
1051     set_userenv($holdingbranch);
1052
1053     my $issue = AddIssue( $patron_1, $item->{barcode} );
1054     is( ref($issue), 'Koha::Schema::Result::Issue' );    # FIXME Should be Koha::Checkout
1055
1056     my ( $error, $question, $alerts );
1057
1058     # AllowReturnToBranch == anywhere
1059     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1060     ## Can be issued from homebranch
1061     set_userenv($homebranch);
1062     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1063     is( keys(%$error) + keys(%$alerts),        0 );
1064     is( exists $question->{ISSUED_TO_ANOTHER}, 1 );
1065     ## Can be issued from holdingbranch
1066     set_userenv($holdingbranch);
1067     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1068     is( keys(%$error) + keys(%$alerts),        0 );
1069     is( exists $question->{ISSUED_TO_ANOTHER}, 1 );
1070     ## Can be issued from another branch
1071     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1072     is( keys(%$error) + keys(%$alerts),        0 );
1073     is( exists $question->{ISSUED_TO_ANOTHER}, 1 );
1074
1075     # AllowReturnToBranch == holdingbranch
1076     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1077     ## Cannot be issued from homebranch
1078     set_userenv($homebranch);
1079     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1080     is( keys(%$question) + keys(%$alerts),  0 );
1081     is( exists $error->{RETURN_IMPOSSIBLE}, 1 );
1082     is( $error->{branch_to_return},         $holdingbranch->{branchcode} );
1083     ## Can be issued from holdinbranch
1084     set_userenv($holdingbranch);
1085     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1086     is( keys(%$error) + keys(%$alerts),        0 );
1087     is( exists $question->{ISSUED_TO_ANOTHER}, 1 );
1088     ## Cannot be issued from another branch
1089     set_userenv($otherbranch);
1090     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1091     is( keys(%$question) + keys(%$alerts),  0 );
1092     is( exists $error->{RETURN_IMPOSSIBLE}, 1 );
1093     is( $error->{branch_to_return},         $holdingbranch->{branchcode} );
1094
1095     # AllowReturnToBranch == homebranch
1096     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1097     ## Can be issued from holdinbranch
1098     set_userenv($homebranch);
1099     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1100     is( keys(%$error) + keys(%$alerts),        0 );
1101     is( exists $question->{ISSUED_TO_ANOTHER}, 1 );
1102     ## Cannot be issued from holdinbranch
1103     set_userenv($holdingbranch);
1104     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1105     is( keys(%$question) + keys(%$alerts),  0 );
1106     is( exists $error->{RETURN_IMPOSSIBLE}, 1 );
1107     is( $error->{branch_to_return},         $homebranch->{branchcode} );
1108     ## Cannot be issued from holdinbranch
1109     set_userenv($otherbranch);
1110     ( $error, $question, $alerts ) = CanBookBeIssued( $patron_2, $item->{barcode} );
1111     is( keys(%$question) + keys(%$alerts),  0 );
1112     is( exists $error->{RETURN_IMPOSSIBLE}, 1 );
1113     is( $error->{branch_to_return},         $homebranch->{branchcode} );
1114
1115     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1116 };
1117
1118 subtest 'AddIssue & AllowReturnToBranch' => sub {
1119     plan tests => 9;
1120
1121     my $homebranch    = $builder->build( { source => 'Branch' } );
1122     my $holdingbranch = $builder->build( { source => 'Branch' } );
1123     my $otherbranch   = $builder->build( { source => 'Branch' } );
1124     my $patron_1      = $builder->build( { source => 'Borrower' } );
1125     my $patron_2      = $builder->build( { source => 'Borrower' } );
1126
1127     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1128     my $item = $builder->build(
1129         {   source => 'Item',
1130             value  => {
1131                 homebranch    => $homebranch->{branchcode},
1132                 holdingbranch => $holdingbranch->{branchcode},
1133                 notforloan    => 0,
1134                 itemlost      => 0,
1135                 withdrawn     => 0,
1136                 biblionumber  => $biblioitem->{biblionumber}
1137             }
1138         }
1139     );
1140
1141     set_userenv($holdingbranch);
1142
1143     my $ref_issue = 'Koha::Schema::Result::Issue'; # FIXME Should be Koha::Checkout
1144     my $issue = AddIssue( $patron_1, $item->{barcode} );
1145
1146     my ( $error, $question, $alerts );
1147
1148     # AllowReturnToBranch == homebranch
1149     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
1150     ## Can be issued from homebranch
1151     set_userenv($homebranch);
1152     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1153     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1154     ## Can be issued from holdinbranch
1155     set_userenv($holdingbranch);
1156     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1157     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1158     ## Can be issued from another branch
1159     set_userenv($otherbranch);
1160     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1161     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1162
1163     # AllowReturnToBranch == holdinbranch
1164     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
1165     ## Cannot be issued from homebranch
1166     set_userenv($homebranch);
1167     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1168     ## Can be issued from holdingbranch
1169     set_userenv($holdingbranch);
1170     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1171     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1172     ## Cannot be issued from another branch
1173     set_userenv($otherbranch);
1174     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1175
1176     # AllowReturnToBranch == homebranch
1177     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
1178     ## Can be issued from homebranch
1179     set_userenv($homebranch);
1180     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), $ref_issue );
1181     set_userenv($holdingbranch); AddIssue( $patron_1, $item->{barcode} ); # Reinsert the original issue
1182     ## Cannot be issued from holdinbranch
1183     set_userenv($holdingbranch);
1184     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1185     ## Cannot be issued from another branch
1186     set_userenv($otherbranch);
1187     is ( ref( AddIssue( $patron_2, $item->{barcode} ) ), '' );
1188     # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1189 };
1190
1191 subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1192     plan tests => 8;
1193
1194     my $library = $builder->build( { source => 'Branch' } );
1195     my $patron  = $builder->build( { source => 'Borrower' } );
1196
1197     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1198     my $item_1 = $builder->build(
1199         {   source => 'Item',
1200             value  => {
1201                 homebranch    => $library->{branchcode},
1202                 holdingbranch => $library->{branchcode},
1203                 notforloan    => 0,
1204                 itemlost      => 0,
1205                 withdrawn     => 0,
1206                 biblionumber  => $biblioitem_1->{biblionumber}
1207             }
1208         }
1209     );
1210     my $biblioitem_2 = $builder->build( { source => 'Biblioitem' } );
1211     my $item_2 = $builder->build(
1212         {   source => 'Item',
1213             value  => {
1214                 homebranch    => $library->{branchcode},
1215                 holdingbranch => $library->{branchcode},
1216                 notforloan    => 0,
1217                 itemlost      => 0,
1218                 withdrawn     => 0,
1219                 biblionumber  => $biblioitem_2->{biblionumber}
1220             }
1221         }
1222     );
1223
1224     my ( $error, $question, $alerts );
1225
1226     # Patron cannot issue item_1, he has overdues
1227     my $yesterday = DateTime->today( time_zone => C4::Context->tz() )->add( days => -1 );
1228     my $issue = AddIssue( $patron, $item_1->{barcode}, $yesterday );    # Add an overdue
1229
1230     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'confirmation' );
1231     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1232     is( keys(%$error) + keys(%$alerts),  0 );
1233     is( $question->{USERBLOCKEDOVERDUE}, 1 );
1234
1235     t::lib::Mocks::mock_preference( 'OverduesBlockCirc', 'block' );
1236     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1237     is( keys(%$question) + keys(%$alerts), 0 );
1238     is( $error->{USERBLOCKEDOVERDUE},      1 );
1239
1240     # Patron cannot issue item_1, he is debarred
1241     my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 );
1242     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->{borrowernumber}, expiration => $tomorrow } );
1243     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1244     is( keys(%$question) + keys(%$alerts), 0 );
1245     is( $error->{USERBLOCKEDWITHENDDATE}, output_pref( { dt => $tomorrow, dateformat => 'sql', dateonly => 1 } ) );
1246
1247     Koha::Patron::Debarments::AddDebarment( { borrowernumber => $patron->{borrowernumber} } );
1248     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1249     is( keys(%$question) + keys(%$alerts), 0 );
1250     is( $error->{USERBLOCKEDNOENDDATE},    '9999-12-31' );
1251 };
1252
1253 subtest 'MultipleReserves' => sub {
1254     plan tests => 3;
1255
1256     my $biblio = MARC::Record->new();
1257     my $title = 'Silence in the library';
1258     $biblio->append_fields(
1259         MARC::Field->new('100', ' ', ' ', a => 'Moffat, Steven'),
1260         MARC::Field->new('245', ' ', ' ', a => $title),
1261     );
1262
1263     my ($biblionumber, $biblioitemnumber) = AddBiblio($biblio, '');
1264
1265     my $branch = $library2->{branchcode};
1266
1267     my $barcode1 = 'R00110001';
1268     my ( $item_bibnum1, $item_bibitemnum1, $itemnumber1 ) = AddItem(
1269         {
1270             homebranch       => $branch,
1271             holdingbranch    => $branch,
1272             barcode          => $barcode1,
1273             replacementprice => 12.00,
1274             itype            => $itemtype
1275         },
1276         $biblionumber
1277     );
1278
1279     my $barcode2 = 'R00110002';
1280     my ( $item_bibnum2, $item_bibitemnum2, $itemnumber2 ) = AddItem(
1281         {
1282             homebranch       => $branch,
1283             holdingbranch    => $branch,
1284             barcode          => $barcode2,
1285             replacementprice => 12.00,
1286             itype            => $itemtype
1287         },
1288         $biblionumber
1289     );
1290
1291     my $bibitems       = '';
1292     my $priority       = '1';
1293     my $resdate        = undef;
1294     my $expdate        = undef;
1295     my $notes          = '';
1296     my $checkitem      = undef;
1297     my $found          = undef;
1298
1299     my %renewing_borrower_data = (
1300         firstname =>  'John',
1301         surname => 'Renewal',
1302         categorycode => 'S',
1303         branchcode => $branch,
1304     );
1305     my $renewing_borrowernumber = AddMember(%renewing_borrower_data);
1306     my $renewing_borrower = GetMember( borrowernumber => $renewing_borrowernumber );
1307     my $issue = AddIssue( $renewing_borrower, $barcode1);
1308     my $datedue = dt_from_string( $issue->date_due() );
1309     is (defined $issue->date_due(), 1, "item 1 checked out");
1310     my $borrowing_borrowernumber = GetItemIssue($itemnumber1)->{borrowernumber};
1311
1312     my %reserving_borrower_data1 = (
1313         firstname =>  'Katrin',
1314         surname => 'Reservation',
1315         categorycode => 'S',
1316         branchcode => $branch,
1317     );
1318     my $reserving_borrowernumber1 = AddMember(%reserving_borrower_data1);
1319     AddReserve(
1320         $branch, $reserving_borrowernumber1, $biblionumber,
1321         $bibitems,  $priority, $resdate, $expdate, $notes,
1322         $title, $checkitem, $found
1323     );
1324
1325     my %reserving_borrower_data2 = (
1326         firstname =>  'Kirk',
1327         surname => 'Reservation',
1328         categorycode => 'S',
1329         branchcode => $branch,
1330     );
1331     my $reserving_borrowernumber2 = AddMember(%reserving_borrower_data2);
1332     AddReserve(
1333         $branch, $reserving_borrowernumber2, $biblionumber,
1334         $bibitems,  $priority, $resdate, $expdate, $notes,
1335         $title, $checkitem, $found
1336     );
1337
1338     {
1339         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber1, 1);
1340         is($renewokay, 0, 'Bug 17641 - should cover the case where 2 books are both reserved, so failing');
1341     }
1342
1343     my $barcode3 = 'R00110003';
1344     my ( $item_bibnum3, $item_bibitemnum3, $itemnumber3 ) = AddItem(
1345         {
1346             homebranch       => $branch,
1347             holdingbranch    => $branch,
1348             barcode          => $barcode3,
1349             replacementprice => 12.00,
1350             itype            => $itemtype
1351         },
1352         $biblionumber
1353     );
1354
1355     {
1356         my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $itemnumber1, 1);
1357         is($renewokay, 1, 'Bug 17641 - should cover the case where 2 books are reserved, but a third one is available');
1358     }
1359 };
1360
1361 subtest 'CanBookBeIssued + AllowMultipleIssuesOnABiblio' => sub {
1362     plan tests => 5;
1363
1364     my $library = $builder->build( { source => 'Branch' } );
1365     my $patron  = $builder->build( { source => 'Borrower' } );
1366
1367     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
1368     my $biblionumber = $biblioitem->{biblionumber};
1369     my $item_1 = $builder->build(
1370         {   source => 'Item',
1371             value  => {
1372                 homebranch    => $library->{branchcode},
1373                 holdingbranch => $library->{branchcode},
1374                 notforloan    => 0,
1375                 itemlost      => 0,
1376                 withdrawn     => 0,
1377                 biblionumber  => $biblionumber,
1378             }
1379         }
1380     );
1381     my $item_2 = $builder->build(
1382         {   source => 'Item',
1383             value  => {
1384                 homebranch    => $library->{branchcode},
1385                 holdingbranch => $library->{branchcode},
1386                 notforloan    => 0,
1387                 itemlost      => 0,
1388                 withdrawn     => 0,
1389                 biblionumber  => $biblionumber,
1390             }
1391         }
1392     );
1393
1394     my ( $error, $question, $alerts );
1395     my $issue = AddIssue( $patron, $item_1->{barcode}, dt_from_string->add( days => 1 ) );
1396
1397     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1398     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1399     is( keys(%$error) + keys(%$alerts),  0, 'No error or alert should be raised' );
1400     is( $question->{BIBLIO_ALREADY_ISSUED}, 1, 'BIBLIO_ALREADY_ISSUED question flag should be set if AllowMultipleIssuesOnABiblio=0 and issue already exists' );
1401
1402     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1403     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1404     is( keys(%$error) + keys(%$question) + keys(%$alerts),  0, 'No BIBLIO_ALREADY_ISSUED flag should be set if AllowMultipleIssuesOnABiblio=1' );
1405
1406     # Add a subscription
1407     Koha::Subscription->new({ biblionumber => $biblionumber })->store;
1408
1409     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0);
1410     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1411     is( keys(%$error) + keys(%$question) + keys(%$alerts),  0, 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription' );
1412
1413     t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1);
1414     ( $error, $question, $alerts ) = CanBookBeIssued( $patron, $item_2->{barcode} );
1415     is( keys(%$error) + keys(%$question) + keys(%$alerts),  0, 'No BIBLIO_ALREADY_ISSUED flag should be set if it is a subscription' );
1416 };
1417
1418 subtest 'AddReturn + CumulativeRestrictionPeriods' => sub {
1419     plan tests => 8;
1420
1421     my $library = $builder->build( { source => 'Branch' } );
1422     my $patron  = $builder->build( { source => 'Borrower' } );
1423
1424     # Add 2 items
1425     my $biblioitem_1 = $builder->build( { source => 'Biblioitem' } );
1426     my $item_1 = $builder->build(
1427         {
1428             source => 'Item',
1429             value  => {
1430                 homebranch    => $library->{branchcode},
1431                 holdingbranch => $library->{branchcode},
1432                 notforloan    => 0,
1433                 itemlost      => 0,
1434                 withdrawn     => 0,
1435                 biblionumber  => $biblioitem_1->{biblionumber}
1436             }
1437         }
1438     );
1439     my $biblioitem_2 = $builder->build( { source => 'Biblioitem' } );
1440     my $item_2 = $builder->build(
1441         {
1442             source => 'Item',
1443             value  => {
1444                 homebranch    => $library->{branchcode},
1445                 holdingbranch => $library->{branchcode},
1446                 notforloan    => 0,
1447                 itemlost      => 0,
1448                 withdrawn     => 0,
1449                 biblionumber  => $biblioitem_2->{biblionumber}
1450             }
1451         }
1452     );
1453
1454     # And the issuing rule
1455     Koha::IssuingRules->search->delete;
1456     my $rule = Koha::IssuingRule->new(
1457         {
1458             categorycode => '*',
1459             itemtype     => '*',
1460             branchcode   => '*',
1461             maxissueqty  => 99,
1462             issuelength  => 1,
1463             firstremind  => 1,        # 1 day of grace
1464             finedays     => 2,        # 2 days of fine per day of overdue
1465             lengthunit   => 'days',
1466         }
1467     );
1468     $rule->store();
1469
1470     # Patron cannot issue item_1, he has overdues
1471     my $five_days_ago = dt_from_string->subtract( days => 5 );
1472     my $ten_days_ago  = dt_from_string->subtract( days => 10 );
1473     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
1474     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
1475       ;    # Add another overdue
1476
1477     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '0' );
1478     AddReturn( $item_1->{barcode}, $library->{branchcode},
1479         undef, undef, dt_from_string );
1480     my $debarments = Koha::Patron::Debarments::GetDebarments(
1481         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1482     is( scalar(@$debarments), 1 );
1483
1484     # FIXME Is it right? I'd have expected 5 * 2 - 1 instead
1485     # Same for the others
1486     my $expected_expiration = output_pref(
1487         {
1488             dt         => dt_from_string->add( days => ( 5 - 1 ) * 2 ),
1489             dateformat => 'sql',
1490             dateonly   => 1
1491         }
1492     );
1493     is( $debarments->[0]->{expiration}, $expected_expiration );
1494
1495     AddReturn( $item_2->{barcode}, $library->{branchcode},
1496         undef, undef, dt_from_string );
1497     $debarments = Koha::Patron::Debarments::GetDebarments(
1498         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1499     is( scalar(@$debarments), 1 );
1500     $expected_expiration = output_pref(
1501         {
1502             dt         => dt_from_string->add( days => ( 10 - 1 ) * 2 ),
1503             dateformat => 'sql',
1504             dateonly   => 1
1505         }
1506     );
1507     is( $debarments->[0]->{expiration}, $expected_expiration );
1508
1509     Koha::Patron::Debarments::DelUniqueDebarment(
1510         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1511
1512     t::lib::Mocks::mock_preference( 'CumulativeRestrictionPeriods', '1' );
1513     AddIssue( $patron, $item_1->{barcode}, $five_days_ago );    # Add an overdue
1514     AddIssue( $patron, $item_2->{barcode}, $ten_days_ago )
1515       ;    # Add another overdue
1516     AddReturn( $item_1->{barcode}, $library->{branchcode},
1517         undef, undef, dt_from_string );
1518     $debarments = Koha::Patron::Debarments::GetDebarments(
1519         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1520     is( scalar(@$debarments), 1 );
1521     $expected_expiration = output_pref(
1522         {
1523             dt         => dt_from_string->add( days => ( 5 - 1 ) * 2 ),
1524             dateformat => 'sql',
1525             dateonly   => 1
1526         }
1527     );
1528     is( $debarments->[0]->{expiration}, $expected_expiration );
1529
1530     AddReturn( $item_2->{barcode}, $library->{branchcode},
1531         undef, undef, dt_from_string );
1532     $debarments = Koha::Patron::Debarments::GetDebarments(
1533         { borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
1534     is( scalar(@$debarments), 1 );
1535     $expected_expiration = output_pref(
1536         {
1537             dt => dt_from_string->add( days => ( 5 - 1 ) * 2 + ( 10 - 1 ) * 2 ),
1538             dateformat => 'sql',
1539             dateonly   => 1
1540         }
1541     );
1542     is( $debarments->[0]->{expiration}, $expected_expiration );
1543 };
1544
1545 sub set_userenv {
1546     my ( $library ) = @_;
1547     C4::Context->set_userenv(0,0,0,'firstname','surname', $library->{branchcode}, $library->{branchname}, '', '', '');
1548 }
1549
1550 1;