Bug 24413: Apply AutoRemoveOverduesRestrictions for lost items
[koha-ffzg.git] / C4 / Circulation.pm
1 package C4::Circulation;
2
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22 use DateTime;
23 use POSIX qw( floor );
24 use Koha::DateUtils;
25 use C4::Context;
26 use C4::Stats;
27 use C4::Reserves;
28 use C4::Biblio;
29 use C4::Items;
30 use C4::Members;
31 use C4::Accounts;
32 use C4::ItemCirculationAlertPreference;
33 use C4::Message;
34 use C4::Debug;
35 use C4::Log; # logaction
36 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
37 use C4::RotatingCollections qw(GetCollectionItemBranches);
38 use Algorithm::CheckDigits;
39
40 use Data::Dumper;
41 use Koha::Account;
42 use Koha::AuthorisedValues;
43 use Koha::Biblioitems;
44 use Koha::DateUtils;
45 use Koha::Calendar;
46 use Koha::Checkouts;
47 use Koha::Illrequests;
48 use Koha::Items;
49 use Koha::Patrons;
50 use Koha::Patron::Debarments;
51 use Koha::Database;
52 use Koha::Libraries;
53 use Koha::Account::Lines;
54 use Koha::Holds;
55 use Koha::RefundLostItemFeeRules;
56 use Koha::Account::Lines;
57 use Koha::Account::Offsets;
58 use Koha::Config::SysPrefs;
59 use Koha::Charges::Fees;
60 use Koha::Util::SystemPreferences;
61 use Koha::Checkouts::ReturnClaims;
62 use Carp;
63 use List::MoreUtils qw( uniq any );
64 use Scalar::Util qw( looks_like_number );
65 use Date::Calc qw(
66   Today
67   Today_and_Now
68   Add_Delta_YM
69   Add_Delta_DHMS
70   Date_to_Days
71   Day_of_Week
72   Add_Delta_Days
73 );
74 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
75
76 BEGIN {
77         require Exporter;
78         @ISA    = qw(Exporter);
79
80         # FIXME subs that should probably be elsewhere
81         push @EXPORT, qw(
82                 &barcodedecode
83         &LostItem
84         &ReturnLostItem
85         &GetPendingOnSiteCheckouts
86         );
87
88         # subs to deal with issuing a book
89         push @EXPORT, qw(
90                 &CanBookBeIssued
91                 &CanBookBeRenewed
92                 &AddIssue
93                 &AddRenewal
94                 &GetRenewCount
95         &GetSoonestRenewDate
96         &GetLatestAutoRenewDate
97                 &GetIssuingCharges
98         &GetBranchBorrowerCircRule
99         &GetBranchItemRule
100                 &GetBiblioIssues
101                 &GetOpenIssue
102         &CheckIfIssuedToPatron
103         &IsItemIssued
104         GetTopIssues
105         );
106
107         # subs to deal with returns
108         push @EXPORT, qw(
109                 &AddReturn
110         &MarkIssueReturned
111         );
112
113         # subs to deal with transfers
114         push @EXPORT, qw(
115                 &transferbook
116                 &GetTransfers
117                 &GetTransfersFromTo
118                 &updateWrongTransfer
119                 &DeleteTransfer
120                 &IsBranchTransferAllowed
121                 &CreateBranchTransferLimit
122                 &DeleteBranchTransferLimits
123         &TransferSlip
124         );
125
126     # subs to deal with offline circulation
127     push @EXPORT, qw(
128       &GetOfflineOperations
129       &GetOfflineOperation
130       &AddOfflineOperation
131       &DeleteOfflineOperation
132       &ProcessOfflineOperation
133     );
134 }
135
136 =head1 NAME
137
138 C4::Circulation - Koha circulation module
139
140 =head1 SYNOPSIS
141
142 use C4::Circulation;
143
144 =head1 DESCRIPTION
145
146 The functions in this module deal with circulation, issues, and
147 returns, as well as general information about the library.
148 Also deals with inventory.
149
150 =head1 FUNCTIONS
151
152 =head2 barcodedecode
153
154   $str = &barcodedecode($barcode, [$filter]);
155
156 Generic filter function for barcode string.
157 Called on every circ if the System Pref itemBarcodeInputFilter is set.
158 Will do some manipulation of the barcode for systems that deliver a barcode
159 to circulation.pl that differs from the barcode stored for the item.
160 For proper functioning of this filter, calling the function on the 
161 correct barcode string (items.barcode) should return an unaltered barcode.
162
163 The optional $filter argument is to allow for testing or explicit 
164 behavior that ignores the System Pref.  Valid values are the same as the 
165 System Pref options.
166
167 =cut
168
169 # FIXME -- the &decode fcn below should be wrapped into this one.
170 # FIXME -- these plugins should be moved out of Circulation.pm
171 #
172 sub barcodedecode {
173     my ($barcode, $filter) = @_;
174     my $branch = C4::Context::mybranch();
175     $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
176     $filter or return $barcode;     # ensure filter is defined, else return untouched barcode
177         if ($filter eq 'whitespace') {
178                 $barcode =~ s/\s//g;
179         } elsif ($filter eq 'cuecat') {
180                 chomp($barcode);
181             my @fields = split( /\./, $barcode );
182             my @results = map( decode($_), @fields[ 1 .. $#fields ] );
183             ($#results == 2) and return $results[2];
184         } elsif ($filter eq 'T-prefix') {
185                 if ($barcode =~ /^[Tt](\d)/) {
186                         (defined($1) and $1 eq '0') and return $barcode;
187             $barcode = substr($barcode, 2) + 0;     # FIXME: probably should be substr($barcode, 1)
188                 }
189         return sprintf("T%07d", $barcode);
190         # FIXME: $barcode could be "T1", causing warning: substr outside of string
191         # Why drop the nonzero digit after the T?
192         # Why pass non-digits (or empty string) to "T%07d"?
193         } elsif ($filter eq 'libsuite8') {
194                 unless($barcode =~ m/^($branch)-/i){    #if barcode starts with branch code its in Koha style. Skip it.
195                         if($barcode =~ m/^(\d)/i){      #Some barcodes even start with 0's & numbers and are assumed to have b as the item type in the libsuite8 software
196                                 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
197                         }else{
198                                 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
199                         }
200                 }
201     } elsif ($filter eq 'EAN13') {
202         my $ean = CheckDigits('ean');
203         if ( $ean->is_valid($barcode) ) {
204             #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
205             $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
206         } else {
207             warn "# [$barcode] not valid EAN-13/UPC-A\n";
208         }
209         }
210     return $barcode;    # return barcode, modified or not
211 }
212
213 =head2 decode
214
215   $str = &decode($chunk);
216
217 Decodes a segment of a string emitted by a CueCat barcode scanner and
218 returns it.
219
220 FIXME: Should be replaced with Barcode::Cuecat from CPAN
221 or Javascript based decoding on the client side.
222
223 =cut
224
225 sub decode {
226     my ($encoded) = @_;
227     my $seq =
228       'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
229     my @s = map { index( $seq, $_ ); } split( //, $encoded );
230     my $l = ( $#s + 1 ) % 4;
231     if ($l) {
232         if ( $l == 1 ) {
233             # warn "Error: Cuecat decode parsing failed!";
234             return;
235         }
236         $l = 4 - $l;
237         $#s += $l;
238     }
239     my $r = '';
240     while ( $#s >= 0 ) {
241         my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
242         $r .=
243             chr( ( $n >> 16 ) ^ 67 )
244          .chr( ( $n >> 8 & 255 ) ^ 67 )
245          .chr( ( $n & 255 ) ^ 67 );
246         @s = @s[ 4 .. $#s ];
247     }
248     $r = substr( $r, 0, length($r) - $l );
249     return $r;
250 }
251
252 =head2 transferbook
253
254   ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch, 
255                                             $barcode, $ignore_reserves, $trigger);
256
257 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
258
259 C<$newbranch> is the code for the branch to which the item should be transferred.
260
261 C<$barcode> is the barcode of the item to be transferred.
262
263 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
264 Otherwise, if an item is reserved, the transfer fails.
265
266 C<$trigger> is the enum value for what triggered the transfer.
267
268 Returns three values:
269
270 =over
271
272 =item $dotransfer 
273
274 is true if the transfer was successful.
275
276 =item $messages
277
278 is a reference-to-hash which may have any of the following keys:
279
280 =over
281
282 =item C<BadBarcode>
283
284 There is no item in the catalog with the given barcode. The value is C<$barcode>.
285
286 =item C<DestinationEqualsHolding>
287
288 The item is already at the branch to which it is being transferred. The transfer is nonetheless considered to have failed. The value should be ignored.
289
290 =item C<WasReturned>
291
292 The item was on loan, and C<&transferbook> automatically returned it before transferring it. The value is the borrower number of the patron who had the item.
293
294 =item C<ResFound>
295
296 The item was reserved. The value is a reference-to-hash whose keys are fields from the reserves table of the Koha database, and C<biblioitemnumber>. It also has the key C<ResFound>, whose value is either C<Waiting> or C<Reserved>.
297
298 =item C<WasTransferred>
299
300 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
301
302 =back
303
304 =back
305
306 =cut
307
308 sub transferbook {
309     my ( $tbr, $barcode, $ignoreRs, $trigger ) = @_;
310     my $messages;
311     my $dotransfer      = 1;
312     my $item = Koha::Items->find( { barcode => $barcode } );
313
314     # bad barcode..
315     unless ( $item ) {
316         $messages->{'BadBarcode'} = $barcode;
317         $dotransfer = 0;
318         return ( $dotransfer, $messages );
319     }
320
321     my $itemnumber = $item->itemnumber;
322     # get branches of book...
323     my $hbr = $item->homebranch;
324     my $fbr = $item->holdingbranch;
325
326     # if using Branch Transfer Limits
327     if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
328         my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
329         if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
330             if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
331                 $messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
332                 $dotransfer = 0;
333             }
334         } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
335             $messages->{'NotAllowed'} = $tbr . "::" . $code;
336             $dotransfer = 0;
337         }
338     }
339
340     # can't transfer book if is already there....
341     if ( $fbr eq $tbr ) {
342         $messages->{'DestinationEqualsHolding'} = 1;
343         $dotransfer = 0;
344     }
345
346     # check if it is still issued to someone, return it...
347     my $issue = Koha::Checkouts->find({ itemnumber => $itemnumber });
348     if ( $issue ) {
349         AddReturn( $barcode, $fbr );
350         $messages->{'WasReturned'} = $issue->borrowernumber;
351     }
352
353     # find reserves.....
354     # That'll save a database query.
355     my ( $resfound, $resrec, undef ) =
356       CheckReserves( $itemnumber );
357     if ( $resfound and not $ignoreRs ) {
358         $resrec->{'ResFound'} = $resfound;
359         $messages->{'ResFound'} = $resrec;
360         $dotransfer = 1;
361     }
362
363     #actually do the transfer....
364     if ($dotransfer) {
365         ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
366
367         # don't need to update MARC anymore, we do it in batch now
368         $messages->{'WasTransfered'} = 1;
369
370     }
371     ModDateLastSeen( $itemnumber );
372     return ( $dotransfer, $messages );
373 }
374
375
376 sub TooMany {
377     my $borrower        = shift;
378     my $item_object = shift;
379     my $params = shift;
380     my $onsite_checkout = $params->{onsite_checkout} || 0;
381     my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
382     my $cat_borrower    = $borrower->{'categorycode'};
383     my $dbh             = C4::Context->dbh;
384         my $branch;
385         # Get which branchcode we need
386     $branch = _GetCircControlBranch($item_object->unblessed,$borrower);
387     my $type = $item_object->effective_itemtype;
388
389     # given branch, patron category, and item type, determine
390     # applicable issuing rule
391     my $maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
392         {
393             categorycode => $cat_borrower,
394             itemtype     => $type,
395             branchcode   => $branch,
396             rule_name    => 'maxissueqty',
397         }
398     );
399     my $maxonsiteissueqty_rule = Koha::CirculationRules->get_effective_rule(
400         {
401             categorycode => $cat_borrower,
402             itemtype     => $type,
403             branchcode   => $branch,
404             rule_name    => 'maxonsiteissueqty',
405         }
406     );
407
408
409     # if a rule is found and has a loan limit set, count
410     # how many loans the patron already has that meet that
411     # rule
412     if (defined($maxissueqty_rule) and $maxissueqty_rule->rule_value ne '') {
413         my @bind_params;
414         my $count_query = q|
415             SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
416             FROM issues
417             JOIN items USING (itemnumber)
418         |;
419
420         my $rule_itemtype = $maxissueqty_rule->itemtype;
421         unless ($rule_itemtype) {
422             # matching rule has the default item type, so count only
423             # those existing loans that don't fall under a more
424             # specific rule
425             if (C4::Context->preference('item-level_itypes')) {
426                 $count_query .= " WHERE items.itype NOT IN (
427                                     SELECT itemtype FROM circulation_rules
428                                     WHERE branchcode = ?
429                                     AND   (categorycode = ? OR categorycode = ?)
430                                     AND   itemtype IS NOT NULL
431                                     AND   rule_name = 'maxissueqty'
432                                   ) ";
433             } else {
434                 $count_query .= " JOIN  biblioitems USING (biblionumber)
435                                   WHERE biblioitems.itemtype NOT IN (
436                                     SELECT itemtype FROM circulation_rules
437                                     WHERE branchcode = ?
438                                     AND   (categorycode = ? OR categorycode = ?)
439                                     AND   itemtype IS NOT NULL
440                                     AND   rule_name = 'maxissueqty'
441                                   ) ";
442             }
443             push @bind_params, $maxissueqty_rule->branchcode;
444             push @bind_params, $maxissueqty_rule->categorycode;
445             push @bind_params, $cat_borrower;
446         } else {
447             # rule has specific item type, so count loans of that
448             # specific item type
449             if (C4::Context->preference('item-level_itypes')) {
450                 $count_query .= " WHERE items.itype = ? ";
451             } else { 
452                 $count_query .= " JOIN  biblioitems USING (biblionumber) 
453                                   WHERE biblioitems.itemtype= ? ";
454             }
455             push @bind_params, $type;
456         }
457
458         $count_query .= " AND borrowernumber = ? ";
459         push @bind_params, $borrower->{'borrowernumber'};
460         my $rule_branch = $maxissueqty_rule->branchcode;
461         if ($rule_branch) {
462             if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
463                 $count_query .= " AND issues.branchcode = ? ";
464                 push @bind_params, $rule_branch;
465             } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
466                 ; # if branch is the patron's home branch, then count all loans by patron
467             } else {
468                 $count_query .= " AND items.homebranch = ? ";
469                 push @bind_params, $rule_branch;
470             }
471         }
472
473         my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params );
474
475         my $max_checkouts_allowed = $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef;
476         my $max_onsite_checkouts_allowed = $maxonsiteissueqty_rule ? $maxonsiteissueqty_rule->rule_value : undef;
477
478         if ( $onsite_checkout and $max_onsite_checkouts_allowed ne '' ) {
479             if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed )  {
480                 return {
481                     reason => 'TOO_MANY_ONSITE_CHECKOUTS',
482                     count => $onsite_checkout_count,
483                     max_allowed => $max_onsite_checkouts_allowed,
484                 }
485             }
486         }
487         if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
488             my $delta = $switch_onsite_checkout ? 1 : 0;
489             if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
490                 return {
491                     reason => 'TOO_MANY_CHECKOUTS',
492                     count => $checkout_count,
493                     max_allowed => $max_checkouts_allowed,
494                 };
495             }
496         } elsif ( not $onsite_checkout ) {
497             if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )  {
498                 return {
499                     reason => 'TOO_MANY_CHECKOUTS',
500                     count => $checkout_count - $onsite_checkout_count,
501                     max_allowed => $max_checkouts_allowed,
502                 };
503             }
504         }
505     }
506
507     # Now count total loans against the limit for the branch
508     my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
509     if (defined($branch_borrower_circ_rule->{patron_maxissueqty}) and $branch_borrower_circ_rule->{patron_maxissueqty} ne '') {
510         my @bind_params = ();
511         my $branch_count_query = q|
512             SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
513             FROM issues
514             JOIN items USING (itemnumber)
515             WHERE borrowernumber = ?
516         |;
517         push @bind_params, $borrower->{borrowernumber};
518
519         if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
520             $branch_count_query .= " AND issues.branchcode = ? ";
521             push @bind_params, $branch;
522         } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
523             ; # if branch is the patron's home branch, then count all loans by patron
524         } else {
525             $branch_count_query .= " AND items.homebranch = ? ";
526             push @bind_params, $branch;
527         }
528         my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params );
529         my $max_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxissueqty};
530         my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxonsiteissueqty};
531
532         if ( $onsite_checkout and $max_onsite_checkouts_allowed ne '' ) {
533             if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed )  {
534                 return {
535                     reason => 'TOO_MANY_ONSITE_CHECKOUTS',
536                     count => $onsite_checkout_count,
537                     max_allowed => $max_onsite_checkouts_allowed,
538                 }
539             }
540         }
541         if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
542             my $delta = $switch_onsite_checkout ? 1 : 0;
543             if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
544                 return {
545                     reason => 'TOO_MANY_CHECKOUTS',
546                     count => $checkout_count,
547                     max_allowed => $max_checkouts_allowed,
548                 };
549             }
550         } elsif ( not $onsite_checkout ) {
551             if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )  {
552                 return {
553                     reason => 'TOO_MANY_CHECKOUTS',
554                     count => $checkout_count - $onsite_checkout_count,
555                     max_allowed => $max_checkouts_allowed,
556                 };
557             }
558         }
559     }
560
561     if ( not defined( $maxissueqty_rule ) and not defined($branch_borrower_circ_rule->{patron_maxissueqty}) ) {
562         return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
563     }
564
565     # OK, the patron can issue !!!
566     return;
567 }
568
569 =head2 CanBookBeIssued
570
571   ( $issuingimpossible, $needsconfirmation, [ $alerts ] ) =  CanBookBeIssued( $patron,
572                       $barcode, $duedate, $inprocess, $ignore_reserves, $params );
573
574 Check if a book can be issued.
575
576 C<$issuingimpossible> and C<$needsconfirmation> are hashrefs.
577
578 IMPORTANT: The assumption by users of this routine is that causes blocking
579 the issue are keyed by uppercase labels and other returned
580 data is keyed in lower case!
581
582 =over 4
583
584 =item C<$patron> is a Koha::Patron
585
586 =item C<$barcode> is the bar code of the book being issued.
587
588 =item C<$duedates> is a DateTime object.
589
590 =item C<$inprocess> boolean switch
591
592 =item C<$ignore_reserves> boolean switch
593
594 =item C<$params> Hashref of additional parameters
595
596 Available keys:
597     override_high_holds - Ignore high holds
598     onsite_checkout     - Checkout is an onsite checkout that will not leave the library
599
600 =back
601
602 Returns :
603
604 =over 4
605
606 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
607 Possible values are :
608
609 =back
610
611 =head3 INVALID_DATE 
612
613 sticky due date is invalid
614
615 =head3 GNA
616
617 borrower gone with no address
618
619 =head3 CARD_LOST
620
621 borrower declared it's card lost
622
623 =head3 DEBARRED
624
625 borrower debarred
626
627 =head3 UNKNOWN_BARCODE
628
629 barcode unknown
630
631 =head3 NOT_FOR_LOAN
632
633 item is not for loan
634
635 =head3 WTHDRAWN
636
637 item withdrawn.
638
639 =head3 RESTRICTED
640
641 item is restricted (set by ??)
642
643 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan 
644 could be prevented, but ones that can be overriden by the operator.
645
646 Possible values are :
647
648 =head3 DEBT
649
650 borrower has debts.
651
652 =head3 RENEW_ISSUE
653
654 renewing, not issuing
655
656 =head3 ISSUED_TO_ANOTHER
657
658 issued to someone else.
659
660 =head3 RESERVED
661
662 reserved for someone else.
663
664 =head3 INVALID_DATE
665
666 sticky due date is invalid or due date in the past
667
668 =head3 TOO_MANY
669
670 if the borrower borrows to much things
671
672 =cut
673
674 sub CanBookBeIssued {
675     my ( $patron, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
676     my %needsconfirmation;    # filled with problems that needs confirmations
677     my %issuingimpossible;    # filled with problems that causes the issue to be IMPOSSIBLE
678     my %alerts;               # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
679     my %messages;             # filled with information messages that should be displayed.
680
681     my $onsite_checkout     = $params->{onsite_checkout}     || 0;
682     my $override_high_holds = $params->{override_high_holds} || 0;
683
684     my $item_object = Koha::Items->find({barcode => $barcode });
685
686     # MANDATORY CHECKS - unless item exists, nothing else matters
687     unless ( $item_object ) {
688         $issuingimpossible{UNKNOWN_BARCODE} = 1;
689     }
690     return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
691
692     my $item_unblessed = $item_object->unblessed; # Transition...
693     my $issue = $item_object->checkout;
694     my $biblio = $item_object->biblio;
695
696     my $biblioitem = $biblio->biblioitem;
697     my $effective_itemtype = $item_object->effective_itemtype;
698     my $dbh             = C4::Context->dbh;
699     my $patron_unblessed = $patron->unblessed;
700
701     my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
702     #
703     # DUE DATE is OK ? -- should already have checked.
704     #
705     if ($duedate && ref $duedate ne 'DateTime') {
706         $duedate = dt_from_string($duedate);
707     }
708     my $now = dt_from_string();
709     unless ( $duedate ) {
710         my $issuedate = $now->clone();
711
712         $duedate = CalcDateDue( $issuedate, $effective_itemtype, $circ_library->branchcode, $patron_unblessed );
713
714         # Offline circ calls AddIssue directly, doesn't run through here
715         #  So issuingimpossible should be ok.
716     }
717
718     my $fees = Koha::Charges::Fees->new(
719         {
720             patron    => $patron,
721             library   => $circ_library,
722             item      => $item_object,
723             to_date   => $duedate,
724         }
725     );
726
727     if ($duedate) {
728         my $today = $now->clone();
729         $today->truncate( to => 'minute');
730         if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
731             $needsconfirmation{INVALID_DATE} = output_pref($duedate);
732         }
733     } else {
734             $issuingimpossible{INVALID_DATE} = output_pref($duedate);
735     }
736
737     #
738     # BORROWER STATUS
739     #
740     if ( $patron->category->category_type eq 'X' && (  $item_object->barcode  )) {
741         # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1  .
742         &UpdateStats({
743                      branch => C4::Context->userenv->{'branch'},
744                      type => 'localuse',
745                      itemnumber => $item_object->itemnumber,
746                      itemtype => $effective_itemtype,
747                      borrowernumber => $patron->borrowernumber,
748                      ccode => $item_object->ccode}
749                     );
750         ModDateLastSeen( $item_object->itemnumber ); # FIXME Move to Koha::Item
751         return( { STATS => 1 }, {});
752     }
753
754     if ( $patron->gonenoaddress && $patron->gonenoaddress == 1 ) {
755         $issuingimpossible{GNA} = 1;
756     }
757
758     if ( $patron->lost && $patron->lost == 1 ) {
759         $issuingimpossible{CARD_LOST} = 1;
760     }
761     if ( $patron->is_debarred ) {
762         $issuingimpossible{DEBARRED} = 1;
763     }
764
765     if ( $patron->is_expired ) {
766         $issuingimpossible{EXPIRED} = 1;
767     }
768
769     #
770     # BORROWER STATUS
771     #
772
773     # DEBTS
774     my $account = $patron->account;
775     my $balance = $account->balance;
776     my $non_issues_charges = $account->non_issues_charges;
777     my $other_charges = $balance - $non_issues_charges;
778
779     my $amountlimit = C4::Context->preference("noissuescharge");
780     my $allowfineoverride = C4::Context->preference("AllowFineOverride");
781     my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
782
783     # Check the debt of this patrons guarantees
784     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
785     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
786     if ( defined $no_issues_charge_guarantees ) {
787         my @guarantees = map { $_->guarantee } $patron->guarantee_relationships();
788         my $guarantees_non_issues_charges;
789         foreach my $g ( @guarantees ) {
790             $guarantees_non_issues_charges += $g->account->non_issues_charges;
791         }
792
793         if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
794             $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
795         } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
796             $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
797         } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
798             $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
799         }
800     }
801
802     if ( C4::Context->preference("IssuingInProcess") ) {
803         if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
804             $issuingimpossible{DEBT} = $non_issues_charges;
805         } elsif ( $non_issues_charges > $amountlimit && !$inprocess && $allowfineoverride) {
806             $needsconfirmation{DEBT} = $non_issues_charges;
807         } elsif ( $allfinesneedoverride && $non_issues_charges > 0 && $non_issues_charges <= $amountlimit && !$inprocess ) {
808             $needsconfirmation{DEBT} = $non_issues_charges;
809         }
810     }
811     else {
812         if ( $non_issues_charges > $amountlimit && $allowfineoverride ) {
813             $needsconfirmation{DEBT} = $non_issues_charges;
814         } elsif ( $non_issues_charges > $amountlimit && !$allowfineoverride) {
815             $issuingimpossible{DEBT} = $non_issues_charges;
816         } elsif ( $non_issues_charges > 0 && $allfinesneedoverride ) {
817             $needsconfirmation{DEBT} = $non_issues_charges;
818         }
819     }
820
821     if ($balance > 0 && $other_charges > 0) {
822         $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
823     }
824
825     $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
826     $patron_unblessed = $patron->unblessed;
827
828     if ( my $debarred_date = $patron->is_debarred ) {
829          # patron has accrued fine days or has a restriction. $count is a date
830         if ($debarred_date eq '9999-12-31') {
831             $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
832         }
833         else {
834             $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
835         }
836     } elsif ( my $num_overdues = $patron->has_overdues ) {
837         ## patron has outstanding overdue loans
838         if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
839             $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
840         }
841         elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
842             $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
843         }
844     }
845
846     #
847     # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
848     #
849     if ( $issue && $issue->borrowernumber eq $patron->borrowernumber ){
850
851         # Already issued to current borrower.
852         # If it is an on-site checkout if it can be switched to a normal checkout
853         # or ask whether the loan should be renewed
854
855         if ( $issue->onsite_checkout
856                 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
857             $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
858         } else {
859             my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
860                 $patron->borrowernumber,
861                 $item_object->itemnumber,
862             );
863             if ( $CanBookBeRenewed == 0 ) {    # no more renewals allowed
864                 if ( $renewerror eq 'onsite_checkout' ) {
865                     $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
866                 }
867                 else {
868                     $issuingimpossible{NO_MORE_RENEWALS} = 1;
869                 }
870             }
871             else {
872                 $needsconfirmation{RENEW_ISSUE} = 1;
873             }
874         }
875     }
876     elsif ( $issue ) {
877
878         # issued to someone else
879
880         my $patron = Koha::Patrons->find( $issue->borrowernumber );
881
882         my ( $can_be_returned, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
883
884         unless ( $can_be_returned ) {
885             $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
886             $issuingimpossible{branch_to_return} = $message;
887         } else {
888             if ( C4::Context->preference('AutoReturnCheckedOutItems') ) {
889                 $alerts{RETURNED_FROM_ANOTHER} = { patron => $patron };
890             } else {
891             $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
892             $needsconfirmation{issued_firstname} = $patron->firstname;
893             $needsconfirmation{issued_surname} = $patron->surname;
894             $needsconfirmation{issued_cardnumber} = $patron->cardnumber;
895             $needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
896             }
897         }
898     }
899
900     # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
901     #
902     my $switch_onsite_checkout = (
903           C4::Context->preference('SwitchOnSiteCheckouts')
904       and $issue
905       and $issue->onsite_checkout
906       and $issue->borrowernumber == $patron->borrowernumber ? 1 : 0 );
907     my $toomany = TooMany( $patron_unblessed, $item_object, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
908     # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
909     if ( $toomany && not exists $needsconfirmation{RENEW_ISSUE} ) {
910         if ( $toomany->{max_allowed} == 0 ) {
911             $needsconfirmation{PATRON_CANT} = 1;
912         }
913         if ( C4::Context->preference("AllowTooManyOverride") ) {
914             $needsconfirmation{TOO_MANY} = $toomany->{reason};
915             $needsconfirmation{current_loan_count} = $toomany->{count};
916             $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
917         } else {
918             $issuingimpossible{TOO_MANY} = $toomany->{reason};
919             $issuingimpossible{current_loan_count} = $toomany->{count};
920             $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
921         }
922     }
923
924     #
925     # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
926     #
927     $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
928     my $wants_check = $patron->wants_check_for_previous_checkout;
929     $needsconfirmation{PREVISSUE} = 1
930         if ($wants_check and $patron->do_check_for_previous_checkout($item_unblessed));
931
932     #
933     # ITEM CHECKING
934     #
935     if ( $item_object->notforloan )
936     {
937         if(!C4::Context->preference("AllowNotForLoanOverride")){
938             $issuingimpossible{NOT_FOR_LOAN} = 1;
939             $issuingimpossible{item_notforloan} = $item_object->notforloan;
940         }else{
941             $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
942             $needsconfirmation{item_notforloan} = $item_object->notforloan;
943         }
944     }
945     else {
946         # we have to check itemtypes.notforloan also
947         if (C4::Context->preference('item-level_itypes')){
948             # this should probably be a subroutine
949             my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
950             $sth->execute($effective_itemtype);
951             my $notforloan=$sth->fetchrow_hashref();
952             if ($notforloan->{'notforloan'}) {
953                 if (!C4::Context->preference("AllowNotForLoanOverride")) {
954                     $issuingimpossible{NOT_FOR_LOAN} = 1;
955                     $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
956                 } else {
957                     $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
958                     $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
959                 }
960             }
961         }
962         else {
963             my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype);
964             if ( $itemtype && defined $itemtype->notforloan && $itemtype->notforloan == 1){
965                 if (!C4::Context->preference("AllowNotForLoanOverride")) {
966                     $issuingimpossible{NOT_FOR_LOAN} = 1;
967                     $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
968                 } else {
969                     $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
970                     $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
971                 }
972             }
973         }
974     }
975     if ( $item_object->withdrawn && $item_object->withdrawn > 0 )
976     {
977         $issuingimpossible{WTHDRAWN} = 1;
978     }
979     if (   $item_object->restricted
980         && $item_object->restricted == 1 )
981     {
982         $issuingimpossible{RESTRICTED} = 1;
983     }
984     if ( $item_object->itemlost && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
985         my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item_object->itemlost });
986         my $code = $av->count ? $av->next->lib : '';
987         $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
988         $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
989     }
990     if ( C4::Context->preference("IndependentBranches") ) {
991         my $userenv = C4::Context->userenv;
992         unless ( C4::Context->IsSuperLibrarian() ) {
993             my $HomeOrHoldingBranch = C4::Context->preference("HomeOrHoldingBranch");
994             if ( $item_object->$HomeOrHoldingBranch ne $userenv->{branch} ){
995                 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
996                 $issuingimpossible{'itemhomebranch'} = $item_object->$HomeOrHoldingBranch;
997             }
998             $needsconfirmation{BORRNOTSAMEBRANCH} = $patron->branchcode
999               if ( $patron->branchcode ne $userenv->{branch} );
1000         }
1001     }
1002
1003     #
1004     # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
1005     #
1006     my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
1007     if ($rentalConfirmation) {
1008         my ($rentalCharge) = GetIssuingCharges( $item_object->itemnumber, $patron->borrowernumber );
1009
1010         my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1011         if ($itemtype_object) {
1012             my $accumulate_charge = $fees->accumulate_rentalcharge();
1013             if ( $accumulate_charge > 0 ) {
1014                 $rentalCharge += $accumulate_charge;
1015             }
1016         }
1017
1018         if ( $rentalCharge > 0 ) {
1019             $needsconfirmation{RENTALCHARGE} = $rentalCharge;
1020         }
1021     }
1022
1023     unless ( $ignore_reserves ) {
1024         # See if the item is on reserve.
1025         my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
1026         if ($restype) {
1027             my $resbor = $res->{'borrowernumber'};
1028             if ( $resbor ne $patron->borrowernumber ) {
1029                 my $patron = Koha::Patrons->find( $resbor );
1030                 if ( $restype eq "Waiting" )
1031                 {
1032                     # The item is on reserve and waiting, but has been
1033                     # reserved by some other patron.
1034                     $needsconfirmation{RESERVE_WAITING} = 1;
1035                     $needsconfirmation{'resfirstname'} = $patron->firstname;
1036                     $needsconfirmation{'ressurname'} = $patron->surname;
1037                     $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1038                     $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1039                     $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1040                     $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1041                     $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1042                 }
1043                 elsif ( $restype eq "Reserved" ) {
1044                     # The item is on reserve for someone else.
1045                     $needsconfirmation{RESERVED} = 1;
1046                     $needsconfirmation{'resfirstname'} = $patron->firstname;
1047                     $needsconfirmation{'ressurname'} = $patron->surname;
1048                     $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1049                     $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1050                     $needsconfirmation{'resbranchcode'} = $patron->branchcode;
1051                     $needsconfirmation{'resreservedate'} = $res->{reservedate};
1052                     $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1053                 }
1054             }
1055         }
1056     }
1057
1058     ## CHECK AGE RESTRICTION
1059     my $agerestriction  = $biblioitem->agerestriction;
1060     my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );
1061     if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1062         if ( C4::Context->preference('AgeRestrictionOverride') ) {
1063             $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1064         }
1065         else {
1066             $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1067         }
1068     }
1069
1070     ## check for high holds decreasing loan period
1071     if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1072         my $check = checkHighHolds( $item_unblessed, $patron_unblessed );
1073
1074         if ( $check->{exceeded} ) {
1075             if ($override_high_holds) {
1076                 $alerts{HIGHHOLDS} = {
1077                     num_holds  => $check->{outstanding},
1078                     duration   => $check->{duration},
1079                     returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1080                 };
1081             }
1082             else {
1083                 $needsconfirmation{HIGHHOLDS} = {
1084                     num_holds  => $check->{outstanding},
1085                     duration   => $check->{duration},
1086                     returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1087                 };
1088             }
1089         }
1090     }
1091
1092     if (
1093         !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1094         # don't do the multiple loans per bib check if we've
1095         # already determined that we've got a loan on the same item
1096         !$issuingimpossible{NO_MORE_RENEWALS} &&
1097         !$needsconfirmation{RENEW_ISSUE}
1098     ) {
1099         # Check if borrower has already issued an item from the same biblio
1100         # Only if it's not a subscription
1101         my $biblionumber = $item_object->biblionumber;
1102         require C4::Serials;
1103         my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1104         unless ($is_a_subscription) {
1105             # FIXME Should be $patron->checkouts($args);
1106             my $checkouts = Koha::Checkouts->search(
1107                 {
1108                     borrowernumber => $patron->borrowernumber,
1109                     biblionumber   => $biblionumber,
1110                 },
1111                 {
1112                     join => 'item',
1113                 }
1114             );
1115             # if we get here, we don't already have a loan on this item,
1116             # so if there are any loans on this bib, ask for confirmation
1117             if ( $checkouts->count ) {
1118                 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1119             }
1120         }
1121     }
1122
1123     return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1124 }
1125
1126 =head2 CanBookBeReturned
1127
1128   ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1129
1130 Check whether the item can be returned to the provided branch
1131
1132 =over 4
1133
1134 =item C<$item> is a hash of item information as returned Koha::Items->find->unblessed (Temporary, should be a Koha::Item instead)
1135
1136 =item C<$branch> is the branchcode where the return is taking place
1137
1138 =back
1139
1140 Returns:
1141
1142 =over 4
1143
1144 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1145
1146 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1147
1148 =back
1149
1150 =cut
1151
1152 sub CanBookBeReturned {
1153   my ($item, $branch) = @_;
1154   my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1155
1156   # assume return is allowed to start
1157   my $allowed = 1;
1158   my $message;
1159
1160   # identify all cases where return is forbidden
1161   if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1162      $allowed = 0;
1163      $message = $item->{'homebranch'};
1164   } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1165      $allowed = 0;
1166      $message = $item->{'holdingbranch'};
1167   } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1168      $allowed = 0;
1169      $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1170   }
1171
1172   return ($allowed, $message);
1173 }
1174
1175 =head2 CheckHighHolds
1176
1177     used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1178     decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1179     has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1180
1181 =cut
1182
1183 sub checkHighHolds {
1184     my ( $item, $borrower ) = @_;
1185     my $branchcode = _GetCircControlBranch( $item, $borrower );
1186     my $item_object = Koha::Items->find( $item->{itemnumber} );
1187
1188     my $return_data = {
1189         exceeded    => 0,
1190         outstanding => 0,
1191         duration    => 0,
1192         due_date    => undef,
1193     };
1194
1195     my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1196
1197     if ( $holds->count() ) {
1198         $return_data->{outstanding} = $holds->count();
1199
1200         my $decreaseLoanHighHoldsControl        = C4::Context->preference('decreaseLoanHighHoldsControl');
1201         my $decreaseLoanHighHoldsValue          = C4::Context->preference('decreaseLoanHighHoldsValue');
1202         my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1203
1204         my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1205
1206         if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1207
1208             # static means just more than a given number of holds on the record
1209
1210             # If the number of holds is less than the threshold, we can stop here
1211             if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1212                 return $return_data;
1213             }
1214         }
1215         elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1216
1217             # dynamic means X more than the number of holdable items on the record
1218
1219             # let's get the items
1220             my @items = $holds->next()->biblio()->items()->as_list;
1221
1222             # Remove any items with status defined to be ignored even if the would not make item unholdable
1223             foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1224                 @items = grep { !$_->$status } @items;
1225             }
1226
1227             # Remove any items that are not holdable for this patron
1228             @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber )->{status} eq 'OK' } @items;
1229
1230             my $items_count = scalar @items;
1231
1232             my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1233
1234             # If the number of holds is less than the count of items we have
1235             # plus the number of holds allowed above that count, we can stop here
1236             if ( $holds->count() <= $threshold ) {
1237                 return $return_data;
1238             }
1239         }
1240
1241         my $issuedate = dt_from_string();
1242
1243         my $calendar = Koha::Calendar->new( branchcode => $branchcode );
1244
1245         my $itype = $item_object->effective_itemtype;
1246         my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1247
1248         my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1249
1250         my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1251         $reduced_datedue->set_hour($orig_due->hour);
1252         $reduced_datedue->set_minute($orig_due->minute);
1253         $reduced_datedue->truncate( to => 'minute' );
1254
1255         if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1256             $return_data->{exceeded} = 1;
1257             $return_data->{duration} = $decreaseLoanHighHoldsDuration;
1258             $return_data->{due_date} = $reduced_datedue;
1259         }
1260     }
1261
1262     return $return_data;
1263 }
1264
1265 =head2 AddIssue
1266
1267   &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1268
1269 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1270
1271 =over 4
1272
1273 =item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
1274
1275 =item C<$barcode> is the barcode of the item being issued.
1276
1277 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1278 Calculated if empty.
1279
1280 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1281
1282 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1283 Defaults to today.  Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1284
1285 AddIssue does the following things :
1286
1287   - step 01: check that there is a borrowernumber & a barcode provided
1288   - check for RENEWAL (book issued & being issued to the same patron)
1289       - renewal YES = Calculate Charge & renew
1290       - renewal NO  =
1291           * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1292           * RESERVE PLACED ?
1293               - fill reserve if reserve to this patron
1294               - cancel reserve or not, otherwise
1295           * TRANSFERT PENDING ?
1296               - complete the transfert
1297           * ISSUE THE BOOK
1298
1299 =back
1300
1301 =cut
1302
1303 sub AddIssue {
1304     my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1305
1306     my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1307     my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1308     my $auto_renew = $params && $params->{auto_renew};
1309     my $dbh          = C4::Context->dbh;
1310     my $barcodecheck = CheckValidBarcode($barcode);
1311
1312     my $issue;
1313
1314     if ( $datedue && ref $datedue ne 'DateTime' ) {
1315         $datedue = dt_from_string($datedue);
1316     }
1317
1318     # $issuedate defaults to today.
1319     if ( !defined $issuedate ) {
1320         $issuedate = dt_from_string();
1321     }
1322     else {
1323         if ( ref $issuedate ne 'DateTime' ) {
1324             $issuedate = dt_from_string($issuedate);
1325
1326         }
1327     }
1328
1329     # Stop here if the patron or barcode doesn't exist
1330     if ( $borrower && $barcode && $barcodecheck ) {
1331         # find which item we issue
1332         my $item_object = Koha::Items->find({ barcode => $barcode })
1333           or return;    # if we don't get an Item, abort.
1334         my $item_unblessed = $item_object->unblessed;
1335
1336         my $branchcode = _GetCircControlBranch( $item_unblessed, $borrower );
1337
1338         # get actual issuing if there is one
1339         my $actualissue = $item_object->checkout;
1340
1341         # check if we just renew the issue.
1342         if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
1343                 and not $switch_onsite_checkout ) {
1344             $datedue = AddRenewal(
1345                 $borrower->{'borrowernumber'},
1346                 $item_object->itemnumber,
1347                 $branchcode,
1348                 $datedue,
1349                 $issuedate,    # here interpreted as the renewal date
1350             );
1351         }
1352         else {
1353             unless ($datedue) {
1354                 my $itype = $item_object->effective_itemtype;
1355                 $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1356
1357             }
1358             $datedue->truncate( to => 'minute' );
1359
1360             my $patron = Koha::Patrons->find( $borrower );
1361             my $library = Koha::Libraries->find( $branchcode );
1362             my $fees = Koha::Charges::Fees->new(
1363                 {
1364                     patron    => $patron,
1365                     library   => $library,
1366                     item      => $item_object,
1367                     to_date   => $datedue,
1368                 }
1369             );
1370
1371             # it's NOT a renewal
1372             if ( $actualissue and not $switch_onsite_checkout ) {
1373                 # This book is currently on loan, but not to the person
1374                 # who wants to borrow it now. mark it returned before issuing to the new borrower
1375                 my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
1376                 return unless $allowed;
1377                 AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
1378             }
1379
1380             C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
1381
1382             # Starting process for transfer job (checking transfert and validate it if we have one)
1383             my ($datesent) = GetTransfers( $item_object->itemnumber );
1384             if ($datesent) {
1385                 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1386                 my $sth = $dbh->prepare(
1387                     "UPDATE branchtransfers 
1388                         SET datearrived = now(),
1389                         tobranch = ?,
1390                         comments = 'Forced branchtransfer'
1391                     WHERE itemnumber= ? AND datearrived IS NULL"
1392                 );
1393                 $sth->execute( C4::Context->userenv->{'branch'},
1394                     $item_object->itemnumber );
1395             }
1396
1397             # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1398             unless ($auto_renew) {
1399                 my $rule = Koha::CirculationRules->get_effective_rule(
1400                     {
1401                         categorycode => $borrower->{categorycode},
1402                         itemtype     => $item_object->effective_itemtype,
1403                         branchcode   => $branchcode,
1404                         rule_name    => 'auto_renew'
1405                     }
1406                 );
1407
1408                 $auto_renew = $rule->rule_value if $rule;
1409             }
1410
1411             # Record in the database the fact that the book was issued.
1412             unless ($datedue) {
1413                 my $itype = $item_object->effective_itemtype;
1414                 $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1415
1416             }
1417             $datedue->truncate( to => 'minute' );
1418
1419             my $issue_attributes = {
1420                 borrowernumber  => $borrower->{'borrowernumber'},
1421                 issuedate       => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1422                 date_due        => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1423                 branchcode      => C4::Context->userenv->{'branch'},
1424                 onsite_checkout => $onsite_checkout,
1425                 auto_renew      => $auto_renew ? 1 : 0,
1426             };
1427
1428             $issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
1429             if ($issue) {
1430                 $issue->set($issue_attributes)->store;
1431             }
1432             else {
1433                 $issue = Koha::Checkout->new(
1434                     {
1435                         itemnumber => $item_object->itemnumber,
1436                         %$issue_attributes,
1437                     }
1438                 )->store;
1439             }
1440             if ( $item_object->location && $item_object->location eq 'CART'
1441                 && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
1442             ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
1443                 CartToShelf( $item_object->itemnumber );
1444             }
1445
1446             if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1447                 UpdateTotalIssues( $item_object->biblionumber, 1 );
1448             }
1449
1450             ## If item was lost, it has now been found, reverse any list item charges if necessary.
1451             if ( $item_object->itemlost ) {
1452                 if (
1453                     Koha::RefundLostItemFeeRules->should_refund(
1454                         {
1455                             current_branch      => C4::Context->userenv->{branch},
1456                             item_home_branch    => $item_object->homebranch,
1457                             item_holding_branch => $item_object->holdingbranch,
1458                         }
1459                     )
1460                   )
1461                 {
1462                     _FixAccountForLostAndFound( $item_object->itemnumber, undef,
1463                         $item_object->barcode );
1464                 }
1465             }
1466
1467             $item_object->issues( ( $item_object->issues || 0 ) + 1);
1468             $item_object->holdingbranch(C4::Context->userenv->{'branch'});
1469             $item_object->itemlost(0);
1470             $item_object->onloan($datedue->ymd());
1471             $item_object->datelastborrowed( dt_from_string()->ymd() );
1472             $item_object->store({log_action => 0});
1473             ModDateLastSeen( $item_object->itemnumber );
1474
1475             # If it costs to borrow this book, charge it to the patron's account.
1476             my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
1477             if ( $charge && $charge > 0 ) {
1478                 AddIssuingCharge( $issue, $charge, 'RENT' );
1479             }
1480
1481             my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1482             if ( $itemtype_object ) {
1483                 my $accumulate_charge = $fees->accumulate_rentalcharge();
1484                 if ( $accumulate_charge > 0 ) {
1485                     AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' );
1486                     $charge += $accumulate_charge;
1487                     $item_unblessed->{charge} = $charge;
1488                 }
1489             }
1490
1491             # Record the fact that this book was issued.
1492             &UpdateStats(
1493                 {
1494                     branch => C4::Context->userenv->{'branch'},
1495                     type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1496                     amount         => $charge,
1497                     other          => ( $sipmode ? "SIP-$sipmode" : '' ),
1498                     itemnumber     => $item_object->itemnumber,
1499                     itemtype       => $item_object->effective_itemtype,
1500                     location       => $item_object->location,
1501                     borrowernumber => $borrower->{'borrowernumber'},
1502                     ccode          => $item_object->ccode,
1503                 }
1504             );
1505
1506             # Send a checkout slip.
1507             my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1508             my %conditions        = (
1509                 branchcode   => $branchcode,
1510                 categorycode => $borrower->{categorycode},
1511                 item_type    => $item_object->effective_itemtype,
1512                 notification => 'CHECKOUT',
1513             );
1514             if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1515                 SendCirculationAlert(
1516                     {
1517                         type     => 'CHECKOUT',
1518                         item     => $item_object->unblessed,
1519                         borrower => $borrower,
1520                         branch   => $branchcode,
1521                     }
1522                 );
1523             }
1524             logaction(
1525                 "CIRCULATION", "ISSUE",
1526                 $borrower->{'borrowernumber'},
1527                 $item_object->itemnumber,
1528             ) if C4::Context->preference("IssueLog");
1529         }
1530     }
1531     return $issue;
1532 }
1533
1534 =head2 GetLoanLength
1535
1536   my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1537
1538 Get loan length for an itemtype, a borrower type and a branch
1539
1540 =cut
1541
1542 sub GetLoanLength {
1543     my ( $categorycode, $itemtype, $branchcode ) = @_;
1544
1545     # Set search precedences
1546     my @params = (
1547         {
1548             categorycode => $categorycode,
1549             itemtype     => $itemtype,
1550             branchcode   => $branchcode,
1551         },
1552         {
1553             categorycode => $categorycode,
1554             itemtype     => undef,
1555             branchcode   => $branchcode,
1556         },
1557         {
1558             categorycode => undef,
1559             itemtype     => $itemtype,
1560             branchcode   => $branchcode,
1561         },
1562         {
1563             categorycode => undef,
1564             itemtype     => undef,
1565             branchcode   => $branchcode,
1566         },
1567         {
1568             categorycode => $categorycode,
1569             itemtype     => $itemtype,
1570             branchcode   => undef,
1571         },
1572         {
1573             categorycode => $categorycode,
1574             itemtype     => undef,
1575             branchcode   => undef,
1576         },
1577         {
1578             categorycode => undef,
1579             itemtype     => $itemtype,
1580             branchcode   => undef,
1581         },
1582         {
1583             categorycode => undef,
1584             itemtype     => undef,
1585             branchcode   => undef,
1586         },
1587     );
1588
1589     # Initialize default values
1590     my $rules = {
1591         issuelength   => 0,
1592         renewalperiod => 0,
1593         lengthunit    => 'days',
1594     };
1595
1596     # Search for rules!
1597     foreach my $rule_name (qw( issuelength renewalperiod lengthunit )) {
1598         foreach my $params (@params) {
1599             my $rule = Koha::CirculationRules->search(
1600                 {
1601                     rule_name => $rule_name,
1602                     %$params,
1603                 }
1604             )->next();
1605
1606             if ($rule) {
1607                 $rules->{$rule_name} = $rule->rule_value;
1608                 last;
1609             }
1610         }
1611     }
1612
1613     return $rules;
1614 }
1615
1616
1617 =head2 GetHardDueDate
1618
1619   my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1620
1621 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1622
1623 =cut
1624
1625 sub GetHardDueDate {
1626     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1627
1628     my $rules = Koha::CirculationRules->get_effective_rules(
1629         {
1630             categorycode => $borrowertype,
1631             itemtype     => $itemtype,
1632             branchcode   => $branchcode,
1633             rules        => [ 'hardduedate', 'hardduedatecompare' ],
1634         }
1635     );
1636
1637     if ( defined( $rules->{hardduedate} ) ) {
1638         if ( $rules->{hardduedate} ) {
1639             return ( dt_from_string( $rules->{hardduedate}, 'iso' ), $rules->{hardduedatecompare} );
1640         }
1641         else {
1642             return ( undef, undef );
1643         }
1644     }
1645 }
1646
1647 =head2 GetBranchBorrowerCircRule
1648
1649   my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1650
1651 Retrieves circulation rule attributes that apply to the given
1652 branch and patron category, regardless of item type.  
1653 The return value is a hashref containing the following key:
1654
1655 patron_maxissueqty - maximum number of loans that a
1656 patron of the given category can have at the given
1657 branch.  If the value is undef, no limit.
1658
1659 patron_maxonsiteissueqty - maximum of on-site checkouts that a
1660 patron of the given category can have at the given
1661 branch.  If the value is undef, no limit.
1662
1663 This will check for different branch/category combinations in the following order:
1664 branch and category
1665 branch only
1666 category only
1667 default branch and category
1668
1669 If no rule has been found in the database, it will default to
1670 the buillt in rule:
1671
1672 patron_maxissueqty - undef
1673 patron_maxonsiteissueqty - undef
1674
1675 C<$branchcode> and C<$categorycode> should contain the
1676 literal branch code and patron category code, respectively - no
1677 wildcards.
1678
1679 =cut
1680
1681 sub GetBranchBorrowerCircRule {
1682     my ( $branchcode, $categorycode ) = @_;
1683
1684     # Initialize default values
1685     my $rules = {
1686         patron_maxissueqty       => undef,
1687         patron_maxonsiteissueqty => undef,
1688     };
1689
1690     # Search for rules!
1691     foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
1692         my $rule = Koha::CirculationRules->get_effective_rule(
1693             {
1694                 categorycode => $categorycode,
1695                 itemtype     => undef,
1696                 branchcode   => $branchcode,
1697                 rule_name    => $rule_name,
1698             }
1699         );
1700
1701         $rules->{$rule_name} = $rule->rule_value if defined $rule;
1702     }
1703
1704     return $rules;
1705 }
1706
1707 =head2 GetBranchItemRule
1708
1709   my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1710
1711 Retrieves circulation rule attributes that apply to the given
1712 branch and item type, regardless of patron category.
1713
1714 The return value is a hashref containing the following keys:
1715
1716 holdallowed => Hold policy for this branch and itemtype. Possible values:
1717   0: No holds allowed.
1718   1: Holds allowed only by patrons that have the same homebranch as the item.
1719   2: Holds allowed from any patron.
1720
1721 returnbranch => branch to which to return item.  Possible values:
1722   noreturn: do not return, let item remain where checked in (floating collections)
1723   homebranch: return to item's home branch
1724   holdingbranch: return to issuer branch
1725
1726 This searches branchitemrules in the following order:
1727
1728   * Same branchcode and itemtype
1729   * Same branchcode, itemtype '*'
1730   * branchcode '*', same itemtype
1731   * branchcode and itemtype '*'
1732
1733 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1734
1735 =cut
1736
1737 sub GetBranchItemRule {
1738     my ( $branchcode, $itemtype ) = @_;
1739
1740     # Search for rules!
1741     my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
1742         {
1743             branchcode => $branchcode,
1744             itemtype => $itemtype,
1745             rule_name => 'holdallowed',
1746         }
1747     );
1748     my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
1749         {
1750             branchcode => $branchcode,
1751             itemtype => $itemtype,
1752             rule_name => 'hold_fulfillment_policy',
1753         }
1754     );
1755     my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
1756         {
1757             branchcode => $branchcode,
1758             itemtype => $itemtype,
1759             rule_name => 'returnbranch',
1760         }
1761     );
1762
1763     # built-in default circulation rule
1764     my $rules;
1765     $rules->{holdallowed} = defined $holdallowed_rule
1766         ? $holdallowed_rule->rule_value
1767         : 2;
1768     $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
1769         ? $hold_fulfillment_policy_rule->rule_value
1770         : 'any';
1771     $rules->{returnbranch} = defined $returnbranch_rule
1772         ? $returnbranch_rule->rule_value
1773         : 'homebranch';
1774
1775     return $rules;
1776 }
1777
1778 =head2 AddReturn
1779
1780   ($doreturn, $messages, $iteminformation, $borrower) =
1781       &AddReturn( $barcode, $branch [,$exemptfine] [,$returndate] );
1782
1783 Returns a book.
1784
1785 =over 4
1786
1787 =item C<$barcode> is the bar code of the book being returned.
1788
1789 =item C<$branch> is the code of the branch where the book is being returned.
1790
1791 =item C<$exemptfine> indicates that overdue charges for the item will be
1792 removed. Optional.
1793
1794 =item C<$return_date> allows the default return date to be overridden
1795 by the given return date. Optional.
1796
1797 =back
1798
1799 C<&AddReturn> returns a list of four items:
1800
1801 C<$doreturn> is true iff the return succeeded.
1802
1803 C<$messages> is a reference-to-hash giving feedback on the operation.
1804 The keys of the hash are:
1805
1806 =over 4
1807
1808 =item C<BadBarcode>
1809
1810 No item with this barcode exists. The value is C<$barcode>.
1811
1812 =item C<NotIssued>
1813
1814 The book is not currently on loan. The value is C<$barcode>.
1815
1816 =item C<withdrawn>
1817
1818 This book has been withdrawn/cancelled. The value should be ignored.
1819
1820 =item C<Wrongbranch>
1821
1822 This book has was returned to the wrong branch.  The value is a hashref
1823 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1824 contain the branchcode of the incorrect and correct return library, respectively.
1825
1826 =item C<ResFound>
1827
1828 The item was reserved. The value is a reference-to-hash whose keys are
1829 fields from the reserves table of the Koha database, and
1830 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1831 either C<Waiting>, C<Reserved>, or 0.
1832
1833 =item C<WasReturned>
1834
1835 Value 1 if return is successful.
1836
1837 =item C<NeedsTransfer>
1838
1839 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1840
1841 =back
1842
1843 C<$iteminformation> is a reference-to-hash, giving information about the
1844 returned item from the issues table.
1845
1846 C<$borrower> is a reference-to-hash, giving information about the
1847 patron who last borrowed the book.
1848
1849 =cut
1850
1851 sub AddReturn {
1852     my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
1853
1854     if ($branch and not Koha::Libraries->find($branch)) {
1855         warn "AddReturn error: branch '$branch' not found.  Reverting to " . C4::Context->userenv->{'branch'};
1856         undef $branch;
1857     }
1858     $branch = C4::Context->userenv->{'branch'} unless $branch;  # we trust userenv to be a safe fallback/default
1859     my $return_date_specified = !!$return_date;
1860     $return_date //= dt_from_string();
1861     my $messages;
1862     my $patron;
1863     my $doreturn       = 1;
1864     my $validTransfert = 0;
1865     my $stat_type = 'return';
1866
1867     # get information on item
1868     my $item = Koha::Items->find({ barcode => $barcode });
1869     unless ($item) {
1870         return ( 0, { BadBarcode => $barcode } );    # no barcode means no item or borrower.  bail out.
1871     }
1872
1873     my $itemnumber = $item->itemnumber;
1874     my $itemtype = $item->effective_itemtype;
1875
1876     my $issue  = $item->checkout;
1877     if ( $issue ) {
1878         $patron = $issue->patron
1879             or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1880                 . Dumper($issue->unblessed) . "\n";
1881     } else {
1882         $messages->{'NotIssued'} = $barcode;
1883         $item->onloan(undef)->store if defined $item->onloan;
1884
1885         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
1886         $doreturn = 0;
1887         # No issue, no borrowernumber.  ONLY if $doreturn, *might* you have a $borrower later.
1888         # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1889         if (C4::Context->preference("RecordLocalUseOnReturn")) {
1890            $messages->{'LocalUse'} = 1;
1891            $stat_type = 'localuse';
1892         }
1893     }
1894
1895         # full item data, but no borrowernumber or checkout info (no issue)
1896     my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
1897         # get the proper branch to which to return the item
1898     my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
1899         # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1900     my $transfer_trigger = $hbr eq 'homebranch' ? 'ReturnToHome' : $hbr eq 'holdingbranch' ? 'ReturnToHolding' : undef;
1901
1902     my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
1903     my $patron_unblessed = $patron ? $patron->unblessed : {};
1904
1905     my $update_loc_rules = get_yaml_pref_hash('UpdateItemLocationOnCheckin');
1906     map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
1907     if ($update_loc_rules) {
1908         if (defined $update_loc_rules->{_ALL_}) {
1909             if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
1910             if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
1911             if ( $item->location ne $update_loc_rules->{_ALL_}) {
1912                 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
1913                 $item->location($update_loc_rules->{_ALL_})->store;
1914             }
1915         }
1916         else {
1917             foreach my $key ( keys %$update_loc_rules ) {
1918                 if ( $update_loc_rules->{$key} eq '_PERM_' ) { $update_loc_rules->{$key} = $item->permanent_location; }
1919                 if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
1920                 if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
1921                     $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
1922                     $item->location($update_loc_rules->{$key})->store;
1923                     last;
1924                 }
1925             }
1926         }
1927     }
1928
1929     my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1930     if ($yaml) {
1931         $yaml = "$yaml\n\n";  # YAML is anal on ending \n. Surplus does not hurt
1932         my $rules;
1933         eval { $rules = YAML::Load($yaml); };
1934         if ($@) {
1935             warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1936         }
1937         else {
1938             foreach my $key ( keys %$rules ) {
1939                 if ( $item->notforloan eq $key ) {
1940                     $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
1941                     $item->notforloan($rules->{$key})->store({ log_action => 0 });
1942                     last;
1943                 }
1944             }
1945         }
1946     }
1947
1948     # check if the return is allowed at this branch
1949     my ($returnallowed, $message) = CanBookBeReturned($item->unblessed, $branch);
1950     unless ($returnallowed){
1951         $messages->{'Wrongbranch'} = {
1952             Wrongbranch => $branch,
1953             Rightbranch => $message
1954         };
1955         $doreturn = 0;
1956         return ( $doreturn, $messages, $issue, $patron_unblessed);
1957     }
1958
1959     if ( $item->withdrawn ) { # book has been cancelled
1960         $messages->{'withdrawn'} = 1;
1961         $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1962     }
1963
1964     if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
1965         $doreturn = 0;
1966     }
1967
1968     # case of a return of document (deal with issues and holdingbranch)
1969     if ($doreturn) {
1970         die "The item is not issed and cannot be returned" unless $issue; # Just in case...
1971         $patron or warn "AddReturn without current borrower";
1972
1973         if ($patron) {
1974             eval {
1975                 MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy );
1976             };
1977             unless ( $@ ) {
1978                 if (
1979                     (
1980                         C4::Context->preference('CalculateFinesOnReturn')
1981                         || ( $return_date_specified && C4::Context->preference('CalculateFinesOnBackdate') )
1982                     )
1983                     && !$item->itemlost
1984                   )
1985                 {
1986                     _CalculateAndUpdateFine( { issue => $issue, item => $item->unblessed, borrower => $patron_unblessed, return_date => $return_date } );
1987                 }
1988             } else {
1989                 carp "The checkin for the following issue failed, Please go to the about page, section 'data corrupted' to know how to fix this problem ($@)" . Dumper( $issue->unblessed );
1990
1991                 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
1992             }
1993
1994             # FIXME is the "= 1" right?  This could be the borrower hash.
1995             $messages->{'WasReturned'} = 1;
1996
1997         }
1998
1999         $item->onloan(undef)->store({ log_action => 0 });
2000     }
2001
2002     # the holdingbranch is updated if the document is returned to another location.
2003     # this is always done regardless of whether the item was on loan or not
2004     my $item_holding_branch = $item->holdingbranch;
2005     if ($item->holdingbranch ne $branch) {
2006         $item->holdingbranch($branch)->store;
2007     }
2008
2009     my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
2010     ModDateLastSeen( $item->itemnumber, $leave_item_lost );
2011
2012     # check if we have a transfer for this document
2013     my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
2014
2015     # if we have a transfer to do, we update the line of transfers with the datearrived
2016     my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber );
2017     if ($datesent) {
2018         if ( $tobranch eq $branch ) {
2019             my $sth = C4::Context->dbh->prepare(
2020                 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
2021             );
2022             $sth->execute( $item->itemnumber );
2023             # if we have a reservation with valid transfer, we can set it's status to 'W'
2024             C4::Reserves::ModReserveStatus($item->itemnumber, 'W');
2025         } else {
2026             $messages->{'WrongTransfer'}     = $tobranch;
2027             $messages->{'WrongTransferItem'} = $item->itemnumber;
2028         }
2029         $validTransfert = 1;
2030     }
2031
2032     # fix up the accounts.....
2033     if ( $item->itemlost ) {
2034         $messages->{'WasLost'} = 1;
2035         unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
2036             if (
2037                 Koha::RefundLostItemFeeRules->should_refund(
2038                     {
2039                         current_branch      => C4::Context->userenv->{branch},
2040                         item_home_branch    => $item->homebranch,
2041                         item_holding_branch => $item_holding_branch
2042                     }
2043                 )
2044               )
2045             {
2046                 _FixAccountForLostAndFound( $item->itemnumber,
2047                     $borrowernumber, $barcode );
2048                 $messages->{'LostItemFeeRefunded'} = 1;
2049             }
2050         }
2051     }
2052
2053     # fix up the overdues in accounts...
2054     if ($borrowernumber) {
2055         my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
2056         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->itemnumber...) failed!";  # zero is OK, check defined
2057
2058         if ( $issue and $issue->is_overdue($return_date) ) {
2059         # fix fine days
2060             my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item->unblessed, dt_from_string($issue->date_due), $return_date );
2061             if ($reminder){
2062                 $messages->{'PrevDebarred'} = $debardate;
2063             } else {
2064                 $messages->{'Debarred'} = $debardate if $debardate;
2065             }
2066         # there's no overdue on the item but borrower had been previously debarred
2067         } elsif ( $issue->date_due and $patron->debarred ) {
2068              if ( $patron->debarred eq "9999-12-31") {
2069                 $messages->{'ForeverDebarred'} = $patron->debarred;
2070              } else {
2071                   my $borrower_debar_dt = dt_from_string( $patron->debarred );
2072                   $borrower_debar_dt->truncate(to => 'day');
2073                   my $today_dt = $return_date->clone()->truncate(to => 'day');
2074                   if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2075                       $messages->{'PrevDebarred'} = $patron->debarred;
2076                   }
2077              }
2078         }
2079     }
2080
2081     # find reserves.....
2082     # launch the Checkreserves routine to find any holds
2083     my ($resfound, $resrec);
2084     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2085     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2086     # if a hold is found and is waiting at another branch, change the priority back to 1 and trigger the hold (this will trigger a transfer and update the hold status properly)
2087     if ( $resfound eq "Waiting" and $branch ne $resrec->{branchcode} ) {
2088         my $hold = C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
2089         $resfound = 'Reserved';
2090         $resrec = $hold->unblessed;
2091     }
2092     if ($resfound) {
2093           $resrec->{'ResFound'} = $resfound;
2094         $messages->{'ResFound'} = $resrec;
2095     }
2096
2097     # Record the fact that this book was returned.
2098     UpdateStats({
2099         branch         => $branch,
2100         type           => $stat_type,
2101         itemnumber     => $itemnumber,
2102         itemtype       => $itemtype,
2103         borrowernumber => $borrowernumber,
2104         ccode          => $item->ccode,
2105     });
2106
2107     # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2108     if ( $patron ) {
2109         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2110         my %conditions = (
2111             branchcode   => $branch,
2112             categorycode => $patron->categorycode,
2113             item_type    => $itemtype,
2114             notification => 'CHECKIN',
2115         );
2116         if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2117             SendCirculationAlert({
2118                 type     => 'CHECKIN',
2119                 item     => $item->unblessed,
2120                 borrower => $patron->unblessed,
2121                 branch   => $branch,
2122             });
2123         }
2124
2125         logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2126             if C4::Context->preference("ReturnLog");
2127         }
2128
2129     # Remove any OVERDUES related debarment if the borrower has no overdues
2130     if ( $borrowernumber
2131       && $patron->debarred
2132       && C4::Context->preference('AutoRemoveOverduesRestrictions')
2133       && !Koha::Patrons->find( $borrowernumber )->has_overdues
2134       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2135     ) {
2136         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2137     }
2138
2139     # Check if this item belongs to a biblio record that is attached to an
2140     # ILL request, if it is we need to update the ILL request's status
2141     if (C4::Context->preference('CirculateILL')) {
2142         my $request = Koha::Illrequests->find(
2143             { biblio_id => $item->biblio->biblionumber }
2144         );
2145         $request->status('RET') if $request;
2146     }
2147
2148     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2149     if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2150         my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2151         if  (C4::Context->preference("AutomaticItemReturn"    ) or
2152             (C4::Context->preference("UseBranchTransferLimits") and
2153              ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2154            )) {
2155             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
2156             $debug and warn "item: " . Dumper($item->unblessed);
2157             ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger);
2158             $messages->{'WasTransfered'} = 1;
2159         } else {
2160             $messages->{'NeedsTransfer'} = $returnbranch;
2161             $messages->{'TransferTrigger'} = $transfer_trigger;
2162         }
2163     }
2164
2165     if ( C4::Context->preference('ClaimReturnedLostValue') ) {
2166         my $claims = Koha::Checkouts::ReturnClaims->search(
2167            {
2168                itemnumber => $item->id,
2169                resolution => undef,
2170            }
2171         );
2172
2173         if ( $claims->count ) {
2174             $messages->{ReturnClaims} = $claims;
2175         }
2176     }
2177
2178     return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2179 }
2180
2181 =head2 MarkIssueReturned
2182
2183   MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy);
2184
2185 Unconditionally marks an issue as being returned by
2186 moving the C<issues> row to C<old_issues> and
2187 setting C<returndate> to the current date.
2188
2189 if C<$returndate> is specified (in iso format), it is used as the date
2190 of the return.
2191
2192 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2193 the old_issue is immediately anonymised
2194
2195 Ideally, this function would be internal to C<C4::Circulation>,
2196 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2197 and offline_circ/process_koc.pl.
2198
2199 =cut
2200
2201 sub MarkIssueReturned {
2202     my ( $borrowernumber, $itemnumber, $returndate, $privacy ) = @_;
2203
2204     # Retrieve the issue
2205     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2206
2207     return unless $issue->borrowernumber == $borrowernumber; # If the item is checked out to another patron we do not return it
2208
2209     my $issue_id = $issue->issue_id;
2210
2211     my $anonymouspatron;
2212     if ( $privacy && $privacy == 2 ) {
2213         # The default of 0 will not work due to foreign key constraints
2214         # The anonymisation will fail if AnonymousPatron is not a valid entry
2215         # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2216         # Note that a warning should appear on the about page (System information tab).
2217         $anonymouspatron = C4::Context->preference('AnonymousPatron');
2218         die "Fatal error: the patron ($borrowernumber) has requested their circulation history be anonymized on check-in, but the AnonymousPatron system preference is empty or not set correctly."
2219             unless Koha::Patrons->find( $anonymouspatron );
2220     }
2221
2222     my $schema = Koha::Database->schema;
2223
2224     # FIXME Improve the return value and handle it from callers
2225     $schema->txn_do(sub {
2226
2227         my $patron = Koha::Patrons->find( $borrowernumber );
2228
2229         # Update the returndate value
2230         if ( $returndate ) {
2231             $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2232         }
2233         else {
2234             $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2235         }
2236
2237         # Create the old_issues entry
2238         my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2239
2240         # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2241         if ( $privacy && $privacy == 2) {
2242             $old_checkout->borrowernumber($anonymouspatron)->store;
2243         }
2244
2245         # And finally delete the issue
2246         $issue->delete;
2247
2248         $issue->item->onloan(undef)->store({ log_action => 0 });
2249
2250         if ( C4::Context->preference('StoreLastBorrower') ) {
2251             my $item = Koha::Items->find( $itemnumber );
2252             $item->last_returned_by( $patron );
2253         }
2254
2255         # Remove any OVERDUES related debarment if the borrower has no overdues
2256         if ( C4::Context->preference('AutoRemoveOverduesRestrictions')
2257           && $patron->debarred
2258           && !$patron->has_overdues
2259           && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2260         ) {
2261             DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2262         }
2263
2264     });
2265
2266     return $issue_id;
2267 }
2268
2269 =head2 _debar_user_on_return
2270
2271     _debar_user_on_return($borrower, $item, $datedue, $returndate);
2272
2273 C<$borrower> borrower hashref
2274
2275 C<$item> item hashref
2276
2277 C<$datedue> date due DateTime object
2278
2279 C<$returndate> DateTime object representing the return time
2280
2281 Internal function, called only by AddReturn that calculates and updates
2282  the user fine days, and debars them if necessary.
2283
2284 Should only be called for overdue returns
2285
2286 Calculation of the debarment date has been moved to a separate subroutine _calculate_new_debar_dt
2287 to ease testing.
2288
2289 =cut
2290
2291 sub _calculate_new_debar_dt {
2292     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2293
2294     my $branchcode = _GetCircControlBranch( $item, $borrower );
2295     my $circcontrol = C4::Context->preference('CircControl');
2296     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2297         {   categorycode => $borrower->{categorycode},
2298             itemtype     => $item->{itype},
2299             branchcode   => $branchcode,
2300             rules => [
2301                 'finedays',
2302                 'lengthunit',
2303                 'firstremind',
2304                 'maxsuspensiondays',
2305                 'suspension_chargeperiod',
2306             ]
2307         }
2308     );
2309     my $finedays = $issuing_rule ? $issuing_rule->{finedays} : undef;
2310     my $unit     = $issuing_rule ? $issuing_rule->{lengthunit} : undef;
2311     my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2312
2313     return unless $finedays;
2314
2315     # finedays is in days, so hourly loans must multiply by 24
2316     # thus 1 hour late equals 1 day suspension * finedays rate
2317     $finedays = $finedays * 24 if ( $unit eq 'hours' );
2318
2319     # grace period is measured in the same units as the loan
2320     my $grace =
2321       DateTime::Duration->new( $unit => $issuing_rule->{firstremind} );
2322
2323     my $deltadays = DateTime::Duration->new(
2324         days => $chargeable_units
2325     );
2326
2327     if ( $deltadays->subtract($grace)->is_positive() ) {
2328         my $suspension_days = $deltadays * $finedays;
2329
2330         if ( defined $issuing_rule->{suspension_chargeperiod} && $issuing_rule->{suspension_chargeperiod} > 1 ) {
2331             # No need to / 1 and do not consider / 0
2332             $suspension_days = DateTime::Duration->new(
2333                 days => floor( $suspension_days->in_units('days') / $issuing_rule->{suspension_chargeperiod} )
2334             );
2335         }
2336
2337         # If the max suspension days is < than the suspension days
2338         # the suspension days is limited to this maximum period.
2339         my $max_sd = $issuing_rule->{maxsuspensiondays};
2340         if ( defined $max_sd && $max_sd ne '' ) {
2341             $max_sd = DateTime::Duration->new( days => $max_sd );
2342             $suspension_days = $max_sd
2343               if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2344         }
2345
2346         my ( $has_been_extended );
2347         if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2348             my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2349             if ( $debarment ) {
2350                 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2351                 $has_been_extended = 1;
2352             }
2353         }
2354
2355         my $new_debar_dt;
2356         # Use the calendar or not to calculate the debarment date
2357         if ( C4::Context->preference('SuspensionsCalendar') eq 'noSuspensionsWhenClosed' ) {
2358             my $calendar = Koha::Calendar->new(
2359                 branchcode => $branchcode,
2360                 days_mode  => 'Calendar'
2361             );
2362             $new_debar_dt = $calendar->addDate( $return_date, $suspension_days );
2363         }
2364         else {
2365             $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
2366         }
2367         return $new_debar_dt;
2368     }
2369     return;
2370 }
2371
2372 sub _debar_user_on_return {
2373     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2374
2375     $return_date //= dt_from_string();
2376
2377     my $new_debar_dt = _calculate_new_debar_dt ($borrower, $item, $dt_due, $return_date);
2378
2379     return unless $new_debar_dt;
2380
2381     Koha::Patron::Debarments::AddUniqueDebarment({
2382         borrowernumber => $borrower->{borrowernumber},
2383         expiration     => $new_debar_dt->ymd(),
2384         type           => 'SUSPENSION',
2385     });
2386     # if borrower was already debarred but does not get an extra debarment
2387     my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2388     my ($new_debarment_str, $is_a_reminder);
2389     if ( $borrower->{debarred} && $borrower->{debarred} eq $patron->is_debarred ) {
2390         $is_a_reminder = 1;
2391         $new_debarment_str = $borrower->{debarred};
2392     } else {
2393         $new_debarment_str = $new_debar_dt->ymd();
2394     }
2395     # FIXME Should return a DateTime object
2396     return $new_debarment_str, $is_a_reminder;
2397 }
2398
2399 =head2 _FixOverduesOnReturn
2400
2401    &_FixOverduesOnReturn($borrowernumber, $itemnumber, $exemptfine, $status);
2402
2403 C<$borrowernumber> borrowernumber
2404
2405 C<$itemnumber> itemnumber
2406
2407 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2408
2409 C<$status> ENUM -- reason for fix [ RETURNED, RENEWED, LOST, FORGIVEN ]
2410
2411 Internal function
2412
2413 =cut
2414
2415 sub _FixOverduesOnReturn {
2416     my ( $borrowernumber, $item, $exemptfine, $status ) = @_;
2417     unless( $borrowernumber ) {
2418         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2419         return;
2420     }
2421     unless( $item ) {
2422         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2423         return;
2424     }
2425     unless( $status ) {
2426         warn "_FixOverduesOnReturn() not supplied valid status";
2427         return;
2428     }
2429
2430     my $schema = Koha::Database->schema;
2431
2432     my $result = $schema->txn_do(
2433         sub {
2434             # check for overdue fine
2435             my $accountlines = Koha::Account::Lines->search(
2436                 {
2437                     borrowernumber  => $borrowernumber,
2438                     itemnumber      => $item,
2439                     debit_type_code => 'OVERDUE',
2440                     status          => 'UNRETURNED'
2441                 }
2442             );
2443             return 0 unless $accountlines->count; # no warning, there's just nothing to fix
2444
2445             my $accountline = $accountlines->next;
2446
2447             my $amountoutstanding = $accountline->amountoutstanding;
2448             if ($exemptfine && ($amountoutstanding != 0)) {
2449                 my $account = Koha::Account->new({patron_id => $borrowernumber});
2450                 my $credit = $account->add_credit(
2451                     {
2452                         amount     => $amountoutstanding,
2453                         user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
2454                         library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
2455                         interface  => C4::Context->interface,
2456                         type       => 'FORGIVEN',
2457                         item_id    => $item
2458                     }
2459                 );
2460
2461                 $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
2462
2463                 $accountline->status('FORGIVEN');
2464
2465                 if (C4::Context->preference("FinesLog")) {
2466                     &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2467                 }
2468             } else {
2469                 $accountline->status($status);
2470             }
2471
2472             return $accountline->store();
2473         }
2474     );
2475
2476     return $result;
2477 }
2478
2479 =head2 _FixAccountForLostAndFound
2480
2481   &_FixAccountForLostAndFound($itemnumber, [$borrowernumber, $barcode]);
2482
2483 Finds the most recent lost item charge for this item and refunds the borrower
2484 appropriatly, taking into account any payments or writeoffs already applied
2485 against the charge.
2486
2487 Internal function, not exported, called only by AddReturn.
2488
2489 =cut
2490
2491 sub _FixAccountForLostAndFound {
2492     my $itemnumber     = shift or return;
2493     my $borrowernumber = @_ ? shift : undef;
2494     my $item_id        = @_ ? shift : $itemnumber;  # Send the barcode if you want that logged in the description
2495
2496     my $credit;
2497
2498     # check for charge made for lost book
2499     my $accountlines = Koha::Account::Lines->search(
2500         {
2501             itemnumber      => $itemnumber,
2502             debit_type_code => 'LOST',
2503             status          => [ undef, { '<>' => 'FOUND' } ]
2504         },
2505         {
2506             order_by => { -desc => [ 'date', 'accountlines_id' ] }
2507         }
2508     );
2509
2510     return unless $accountlines->count > 0;
2511     my $accountline     = $accountlines->next;
2512     my $total_to_refund = 0;
2513
2514     return unless $accountline->borrowernumber;
2515     my $patron = Koha::Patrons->find( $accountline->borrowernumber );
2516     return unless $patron; # Patron has been deleted, nobody to credit the return to
2517
2518     my $account = $patron->account;
2519
2520     # Use cases
2521     if ( $accountline->amount > $accountline->amountoutstanding ) {
2522         # some amount has been cancelled. collect the offsets that are not writeoffs
2523         # this works because the only way to subtract from this kind of a debt is
2524         # using the UI buttons 'Pay' and 'Write off'
2525         my $credits_offsets = Koha::Account::Offsets->search({
2526             debit_id  => $accountline->id,
2527             credit_id => { '!=' => undef }, # it is not the debit itself
2528             type      => { '!=' => 'Writeoff' },
2529             amount    => { '<'  => 0 } # credits are negative on the DB
2530         });
2531
2532         $total_to_refund = ( $credits_offsets->count > 0 )
2533                             ? $credits_offsets->total * -1 # credits are negative on the DB
2534                             : 0;
2535     }
2536
2537     my $credit_total = $accountline->amountoutstanding + $total_to_refund;
2538
2539     if ( $credit_total > 0 ) {
2540         my $branchcode = C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
2541         $credit = $account->add_credit(
2542             {
2543                 amount      => $credit_total,
2544                 description => 'Item found ' . $item_id,
2545                 type        => 'LOST_FOUND',
2546                 interface   => C4::Context->interface,
2547                 library_id  => $branchcode,
2548                 item_id     => $itemnumber
2549             }
2550         );
2551
2552         $credit->apply( { debits => [ $accountline ] } );
2553     }
2554
2555     # Update the account status
2556     $accountline->discard_changes->status('FOUND');
2557     $accountline->store;
2558
2559     $accountline->item->paidfor('')->store({ log_action => 0 });
2560
2561     if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
2562         $account->reconcile_balance;
2563     }
2564
2565     return ($credit) ? $credit->id : undef;
2566 }
2567
2568 =head2 _GetCircControlBranch
2569
2570    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2571
2572 Internal function : 
2573
2574 Return the library code to be used to determine which circulation
2575 policy applies to a transaction.  Looks up the CircControl and
2576 HomeOrHoldingBranch system preferences.
2577
2578 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2579
2580 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2581
2582 =cut
2583
2584 sub _GetCircControlBranch {
2585     my ($item, $borrower) = @_;
2586     my $circcontrol = C4::Context->preference('CircControl');
2587     my $branch;
2588
2589     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2590         $branch= C4::Context->userenv->{'branch'};
2591     } elsif ($circcontrol eq 'PatronLibrary') {
2592         $branch=$borrower->{branchcode};
2593     } else {
2594         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2595         $branch = $item->{$branchfield};
2596         # default to item home branch if holdingbranch is used
2597         # and is not defined
2598         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2599             $branch = $item->{homebranch};
2600         }
2601     }
2602     return $branch;
2603 }
2604
2605 =head2 GetOpenIssue
2606
2607   $issue = GetOpenIssue( $itemnumber );
2608
2609 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2610
2611 C<$itemnumber> is the item's itemnumber
2612
2613 Returns a hashref
2614
2615 =cut
2616
2617 sub GetOpenIssue {
2618   my ( $itemnumber ) = @_;
2619   return unless $itemnumber;
2620   my $dbh = C4::Context->dbh;  
2621   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2622   $sth->execute( $itemnumber );
2623   return $sth->fetchrow_hashref();
2624
2625 }
2626
2627 =head2 GetBiblioIssues
2628
2629   $issues = GetBiblioIssues($biblionumber);
2630
2631 this function get all issues from a biblionumber.
2632
2633 Return:
2634 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash contains all column from
2635 tables issues and the firstname,surname & cardnumber from borrowers.
2636
2637 =cut
2638
2639 sub GetBiblioIssues {
2640     my $biblionumber = shift;
2641     return unless $biblionumber;
2642     my $dbh   = C4::Context->dbh;
2643     my $query = "
2644         SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2645         FROM issues
2646             LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2647             LEFT JOIN items ON issues.itemnumber = items.itemnumber
2648             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2649             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2650         WHERE biblio.biblionumber = ?
2651         UNION ALL
2652         SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2653         FROM old_issues
2654             LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2655             LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2656             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2657             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2658         WHERE biblio.biblionumber = ?
2659         ORDER BY timestamp
2660     ";
2661     my $sth = $dbh->prepare($query);
2662     $sth->execute($biblionumber, $biblionumber);
2663
2664     my @issues;
2665     while ( my $data = $sth->fetchrow_hashref ) {
2666         push @issues, $data;
2667     }
2668     return \@issues;
2669 }
2670
2671 =head2 GetUpcomingDueIssues
2672
2673   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2674
2675 =cut
2676
2677 sub GetUpcomingDueIssues {
2678     my $params = shift;
2679
2680     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2681     my $dbh = C4::Context->dbh;
2682
2683     my $statement = <<END_SQL;
2684 SELECT *
2685 FROM (
2686     SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2687     FROM issues
2688     LEFT JOIN items USING (itemnumber)
2689     LEFT OUTER JOIN branches USING (branchcode)
2690     WHERE returndate is NULL
2691 ) tmp
2692 WHERE days_until_due >= 0 AND days_until_due <= ?
2693 END_SQL
2694
2695     my @bind_parameters = ( $params->{'days_in_advance'} );
2696     
2697     my $sth = $dbh->prepare( $statement );
2698     $sth->execute( @bind_parameters );
2699     my $upcoming_dues = $sth->fetchall_arrayref({});
2700
2701     return $upcoming_dues;
2702 }
2703
2704 =head2 CanBookBeRenewed
2705
2706   ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2707
2708 Find out whether a borrowed item may be renewed.
2709
2710 C<$borrowernumber> is the borrower number of the patron who currently
2711 has the item on loan.
2712
2713 C<$itemnumber> is the number of the item to renew.
2714
2715 C<$override_limit>, if supplied with a true value, causes
2716 the limit on the number of times that the loan can be renewed
2717 (as controlled by the item type) to be ignored. Overriding also allows
2718 to renew sooner than "No renewal before" and to manually renew loans
2719 that are automatically renewed.
2720
2721 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2722 item must currently be on loan to the specified borrower; renewals
2723 must be allowed for the item's type; and the borrower must not have
2724 already renewed the loan. $error will contain the reason the renewal can not proceed
2725
2726 =cut
2727
2728 sub CanBookBeRenewed {
2729     my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2730
2731     my $dbh    = C4::Context->dbh;
2732     my $renews = 1;
2733     my $auto_renew = 0;
2734
2735     my $item      = Koha::Items->find($itemnumber)      or return ( 0, 'no_item' );
2736     my $issue = $item->checkout or return ( 0, 'no_checkout' );
2737     return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2738     return ( 0, 'item_denied_renewal') if _item_denied_renewal({ item => $item });
2739
2740     my $patron = $issue->patron or return;
2741
2742     # override_limit will override anything else except on_reserve
2743     unless ( $override_limit ){
2744         my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
2745         my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2746             {
2747                 categorycode => $patron->categorycode,
2748                 itemtype     => $item->effective_itemtype,
2749                 branchcode   => $branchcode,
2750                 rules => [
2751                     'renewalsallowed',
2752                     'no_auto_renewal_after',
2753                     'no_auto_renewal_after_hard_limit',
2754                     'lengthunit',
2755                     'norenewalbefore',
2756                 ]
2757             }
2758         );
2759
2760         return ( 0, "too_many" )
2761           if not $issuing_rule->{renewalsallowed} or $issuing_rule->{renewalsallowed} <= $issue->renewals;
2762
2763         my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2764         my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2765         $patron         = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2766         my $restricted  = $patron->is_debarred;
2767         my $hasoverdues = $patron->has_overdues;
2768
2769         if ( $restricted and $restrictionblockrenewing ) {
2770             return ( 0, 'restriction');
2771         } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2772             return ( 0, 'overdue');
2773         }
2774
2775         if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2776
2777             if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2778                 return ( 0, 'auto_account_expired' );
2779             }
2780
2781             if ( defined $issuing_rule->{no_auto_renewal_after}
2782                     and $issuing_rule->{no_auto_renewal_after} ne "" ) {
2783                 # Get issue_date and add no_auto_renewal_after
2784                 # If this is greater than today, it's too late for renewal.
2785                 my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2786                 $maximum_renewal_date->add(
2787                     $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after}
2788                 );
2789                 my $now = dt_from_string;
2790                 if ( $now >= $maximum_renewal_date ) {
2791                     return ( 0, "auto_too_late" );
2792                 }
2793             }
2794             if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit}
2795                           and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) {
2796                 # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2797                 if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) {
2798                     return ( 0, "auto_too_late" );
2799                 }
2800             }
2801
2802             if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2803                 my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2804                 my $amountoutstanding =
2805                   C4::Context->preference("OPACFineNoRenewalsIncludeCredit")
2806                   ? $patron->account->balance
2807                   : $patron->account->outstanding_debits->total_outstanding;
2808                 if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2809                     return ( 0, "auto_too_much_oweing" );
2810                 }
2811             }
2812         }
2813
2814         if ( defined $issuing_rule->{norenewalbefore}
2815             and $issuing_rule->{norenewalbefore} ne "" )
2816         {
2817
2818             # Calculate soonest renewal by subtracting 'No renewal before' from due date
2819             my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2820                 $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
2821
2822             # Depending on syspref reset the exact time, only check the date
2823             if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2824                 and $issuing_rule->{lengthunit} eq 'days' )
2825             {
2826                 $soonestrenewal->truncate( to => 'day' );
2827             }
2828
2829             if ( $soonestrenewal > dt_from_string() )
2830             {
2831                 return ( 0, "auto_too_soon" ) if $issue->auto_renew && $patron->autorenew_checkouts;
2832                 return ( 0, "too_soon" );
2833             }
2834             elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2835                 $auto_renew = 1;
2836             }
2837         }
2838
2839         # Fallback for automatic renewals:
2840         # If norenewalbefore is undef, don't renew before due date.
2841         if ( $issue->auto_renew && !$auto_renew && $patron->autorenew_checkouts ) {
2842             my $now = dt_from_string;
2843             if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
2844                 $auto_renew = 1;
2845             } else {
2846                 return ( 0, "auto_too_soon" );
2847             }
2848         }
2849     }
2850
2851     my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2852
2853     # This item can fill one or more unfilled reserve, can those unfilled reserves
2854     # all be filled by other available items?
2855     if ( $resfound
2856         && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2857     {
2858         my $schema = Koha::Database->new()->schema();
2859
2860         my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2861         if ($item_holds) {
2862             # There is an item level hold on this item, no other item can fill the hold
2863             $resfound = 1;
2864         }
2865         else {
2866
2867             # Get all other items that could possibly fill reserves
2868             my @itemnumbers = $schema->resultset('Item')->search(
2869                 {
2870                     biblionumber => $resrec->{biblionumber},
2871                     onloan       => undef,
2872                     notforloan   => 0,
2873                     -not         => { itemnumber => $itemnumber }
2874                 },
2875                 { columns => 'itemnumber' }
2876             )->get_column('itemnumber')->all();
2877
2878             # Get all other reserves that could have been filled by this item
2879             my @borrowernumbers;
2880             while (1) {
2881                 my ( $reserve_found, $reserve, undef ) =
2882                   C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2883
2884                 if ($reserve_found) {
2885                     push( @borrowernumbers, $reserve->{borrowernumber} );
2886                 }
2887                 else {
2888                     last;
2889                 }
2890             }
2891
2892             # If the count of the union of the lists of reservable items for each borrower
2893             # is equal or greater than the number of borrowers, we know that all reserves
2894             # can be filled with available items. We can get the union of the sets simply
2895             # by pushing all the elements onto an array and removing the duplicates.
2896             my @reservable;
2897             my %patrons;
2898             ITEM: foreach my $itemnumber (@itemnumbers) {
2899                 my $item = Koha::Items->find( $itemnumber );
2900                 next if IsItemOnHoldAndFound( $itemnumber );
2901                 for my $borrowernumber (@borrowernumbers) {
2902                     my $patron = $patrons{$borrowernumber} //= Koha::Patrons->find( $borrowernumber );
2903                     next unless IsAvailableForItemLevelRequest($item, $patron);
2904                     next unless CanItemBeReserved($borrowernumber,$itemnumber);
2905
2906                     push @reservable, $itemnumber;
2907                     if (@reservable >= @borrowernumbers) {
2908                         $resfound = 0;
2909                         last ITEM;
2910                     }
2911                     last;
2912                 }
2913             }
2914         }
2915     }
2916     return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2917     return ( 0, "auto_renew" ) if $auto_renew && !$override_limit; # 0 if auto-renewal should not succeed
2918
2919     return ( 1, undef );
2920 }
2921
2922 =head2 AddRenewal
2923
2924   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2925
2926 Renews a loan.
2927
2928 C<$borrowernumber> is the borrower number of the patron who currently
2929 has the item.
2930
2931 C<$itemnumber> is the number of the item to renew.
2932
2933 C<$branch> is the library where the renewal took place (if any).
2934            The library that controls the circ policies for the renewal is retrieved from the issues record.
2935
2936 C<$datedue> can be a DateTime object used to set the due date.
2937
2938 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2939 this parameter is not supplied, lastreneweddate is set to the current date.
2940
2941 C<$skipfinecalc> is an optional boolean. There may be circumstances where, even if the
2942 CalculateFinesOnReturn syspref is enabled, we don't want to calculate fines upon renew,
2943 for example, when we're renewing as a result of a fine being paid (see RenewAccruingItemWhenPaid
2944 syspref)
2945
2946 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2947 from the book's item type.
2948
2949 =cut
2950
2951 sub AddRenewal {
2952     my $borrowernumber  = shift;
2953     my $itemnumber      = shift or return;
2954     my $branch          = shift;
2955     my $datedue         = shift;
2956     my $lastreneweddate = shift || dt_from_string();
2957     my $skipfinecalc    = shift;
2958
2959     my $item_object   = Koha::Items->find($itemnumber) or return;
2960     my $biblio = $item_object->biblio;
2961     my $issue  = $item_object->checkout;
2962     my $item_unblessed = $item_object->unblessed;
2963
2964     my $dbh = C4::Context->dbh;
2965
2966     return unless $issue;
2967
2968     $borrowernumber ||= $issue->borrowernumber;
2969
2970     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2971         carp 'Invalid date passed to AddRenewal.';
2972         return;
2973     }
2974
2975     my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
2976     my $patron_unblessed = $patron->unblessed;
2977
2978     my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
2979
2980     my $schema = Koha::Database->schema;
2981     $schema->txn_do(sub{
2982
2983         if ( !$skipfinecalc && C4::Context->preference('CalculateFinesOnReturn') ) {
2984             _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed } );
2985         }
2986         _FixOverduesOnReturn( $borrowernumber, $itemnumber, undef, 'RENEWED' );
2987
2988         # If the due date wasn't specified, calculate it by adding the
2989         # book's loan length to today's date or the current due date
2990         # based on the value of the RenewalPeriodBase syspref.
2991         my $itemtype = $item_object->effective_itemtype;
2992         unless ($datedue) {
2993
2994             $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2995                                             dt_from_string( $issue->date_due, 'sql' ) :
2996                                             dt_from_string();
2997             $datedue =  CalcDateDue($datedue, $itemtype, $circ_library->branchcode, $patron_unblessed, 'is a renewal');
2998         }
2999
3000         my $fees = Koha::Charges::Fees->new(
3001             {
3002                 patron    => $patron,
3003                 library   => $circ_library,
3004                 item      => $item_object,
3005                 from_date => dt_from_string( $issue->date_due, 'sql' ),
3006                 to_date   => dt_from_string($datedue),
3007             }
3008         );
3009
3010         # Update the issues record to have the new due date, and a new count
3011         # of how many times it has been renewed.
3012         my $renews = ( $issue->renewals || 0 ) + 1;
3013         my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
3014                                 WHERE borrowernumber=?
3015                                 AND itemnumber=?"
3016         );
3017
3018         $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
3019
3020         # Update the renewal count on the item, and tell zebra to reindex
3021         $renews = ( $item_object->renewals || 0 ) + 1;
3022         $item_object->renewals($renews);
3023         $item_object->onloan($datedue);
3024         $item_object->store({ log_action => 0 });
3025
3026         # Charge a new rental fee, if applicable
3027         my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
3028         if ( $charge > 0 ) {
3029             AddIssuingCharge($issue, $charge, 'RENT_RENEW');
3030         }
3031
3032         # Charge a new accumulate rental fee, if applicable
3033         my $itemtype_object = Koha::ItemTypes->find( $itemtype );
3034         if ( $itemtype_object ) {
3035             my $accumulate_charge = $fees->accumulate_rentalcharge();
3036             if ( $accumulate_charge > 0 ) {
3037                 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY_RENEW' )
3038             }
3039             $charge += $accumulate_charge;
3040         }
3041
3042         # Send a renewal slip according to checkout alert preferencei
3043         if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
3044             my $circulation_alert = 'C4::ItemCirculationAlertPreference';
3045             my %conditions        = (
3046                 branchcode   => $branch,
3047                 categorycode => $patron->categorycode,
3048                 item_type    => $itemtype,
3049                 notification => 'CHECKOUT',
3050             );
3051             if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
3052                 SendCirculationAlert(
3053                     {
3054                         type     => 'RENEWAL',
3055                         item     => $item_unblessed,
3056                         borrower => $patron->unblessed,
3057                         branch   => $branch,
3058                     }
3059                 );
3060             }
3061         }
3062
3063         # Remove any OVERDUES related debarment if the borrower has no overdues
3064         if ( $patron
3065           && $patron->is_debarred
3066           && ! $patron->has_overdues
3067           && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
3068         ) {
3069             DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
3070         }
3071
3072         # Add the renewal to stats
3073         UpdateStats(
3074             {
3075                 branch         => $item_object->renewal_branchcode({branch => $branch}),
3076                 type           => 'renew',
3077                 amount         => $charge,
3078                 itemnumber     => $itemnumber,
3079                 itemtype       => $itemtype,
3080                 location       => $item_object->location,
3081                 borrowernumber => $borrowernumber,
3082                 ccode          => $item_object->ccode,
3083             }
3084         );
3085
3086         #Log the renewal
3087         logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
3088     });
3089
3090     return $datedue;
3091 }
3092
3093 sub GetRenewCount {
3094     # check renewal status
3095     my ( $bornum, $itemno ) = @_;
3096     my $dbh           = C4::Context->dbh;
3097     my $renewcount    = 0;
3098     my $renewsallowed = 0;
3099     my $renewsleft    = 0;
3100
3101     my $patron = Koha::Patrons->find( $bornum );
3102     my $item   = Koha::Items->find($itemno);
3103
3104     return (0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
3105
3106     # Look in the issues table for this item, lent to this borrower,
3107     # and not yet returned.
3108
3109     # FIXME - I think this function could be redone to use only one SQL call.
3110     my $sth = $dbh->prepare(
3111         "select * from issues
3112                                 where (borrowernumber = ?)
3113                                 and (itemnumber = ?)"
3114     );
3115     $sth->execute( $bornum, $itemno );
3116     my $data = $sth->fetchrow_hashref;
3117     $renewcount = $data->{'renewals'} if $data->{'renewals'};
3118     # $item and $borrower should be calculated
3119     my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
3120
3121     my $rule = Koha::CirculationRules->get_effective_rule(
3122         {
3123             categorycode => $patron->categorycode,
3124             itemtype     => $item->effective_itemtype,
3125             branchcode   => $branchcode,
3126             rule_name    => 'renewalsallowed',
3127         }
3128     );
3129
3130     $renewsallowed = $rule ? $rule->rule_value : 0;
3131     $renewsleft    = $renewsallowed - $renewcount;
3132     if($renewsleft < 0){ $renewsleft = 0; }
3133     return ( $renewcount, $renewsallowed, $renewsleft );
3134 }
3135
3136 =head2 GetSoonestRenewDate
3137
3138   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3139
3140 Find out the soonest possible renew date of a borrowed item.
3141
3142 C<$borrowernumber> is the borrower number of the patron who currently
3143 has the item on loan.
3144
3145 C<$itemnumber> is the number of the item to renew.
3146
3147 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3148 renew date, based on the value "No renewal before" of the applicable
3149 issuing rule. Returns the current date if the item can already be
3150 renewed, and returns undefined if the borrower, loan, or item
3151 cannot be found.
3152
3153 =cut
3154
3155 sub GetSoonestRenewDate {
3156     my ( $borrowernumber, $itemnumber ) = @_;
3157
3158     my $dbh = C4::Context->dbh;
3159
3160     my $item      = Koha::Items->find($itemnumber)      or return;
3161     my $itemissue = $item->checkout or return;
3162
3163     $borrowernumber ||= $itemissue->borrowernumber;
3164     my $patron = Koha::Patrons->find( $borrowernumber )
3165       or return;
3166
3167     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3168     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
3169         {   categorycode => $patron->categorycode,
3170             itemtype     => $item->effective_itemtype,
3171             branchcode   => $branchcode,
3172             rules => [
3173                 'norenewalbefore',
3174                 'lengthunit',
3175             ]
3176         }
3177     );
3178
3179     my $now = dt_from_string;
3180     return $now unless $issuing_rule;
3181
3182     if ( defined $issuing_rule->{norenewalbefore}
3183         and $issuing_rule->{norenewalbefore} ne "" )
3184     {
3185         my $soonestrenewal =
3186           dt_from_string( $itemissue->date_due )->subtract(
3187             $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
3188
3189         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3190             and $issuing_rule->{lengthunit} eq 'days' )
3191         {
3192             $soonestrenewal->truncate( to => 'day' );
3193         }
3194         return $soonestrenewal if $now < $soonestrenewal;
3195     }
3196     return $now;
3197 }
3198
3199 =head2 GetLatestAutoRenewDate
3200
3201   $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3202
3203 Find out the latest possible auto renew date of a borrowed item.
3204
3205 C<$borrowernumber> is the borrower number of the patron who currently
3206 has the item on loan.
3207
3208 C<$itemnumber> is the number of the item to renew.
3209
3210 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3211 auto renew date, based on the value "No auto renewal after" and the "No auto
3212 renewal after (hard limit) of the applicable issuing rule.
3213 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3214 or item cannot be found.
3215
3216 =cut
3217
3218 sub GetLatestAutoRenewDate {
3219     my ( $borrowernumber, $itemnumber ) = @_;
3220
3221     my $dbh = C4::Context->dbh;
3222
3223     my $item      = Koha::Items->find($itemnumber)  or return;
3224     my $itemissue = $item->checkout                 or return;
3225
3226     $borrowernumber ||= $itemissue->borrowernumber;
3227     my $patron = Koha::Patrons->find( $borrowernumber )
3228       or return;
3229
3230     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3231     my $circulation_rules = Koha::CirculationRules->get_effective_rules(
3232         {
3233             categorycode => $patron->categorycode,
3234             itemtype     => $item->effective_itemtype,
3235             branchcode   => $branchcode,
3236             rules => [
3237                 'no_auto_renewal_after',
3238                 'no_auto_renewal_after_hard_limit',
3239                 'lengthunit',
3240             ]
3241         }
3242     );
3243
3244     return unless $circulation_rules;
3245     return
3246       if ( not $circulation_rules->{no_auto_renewal_after}
3247             or $circulation_rules->{no_auto_renewal_after} eq '' )
3248       and ( not $circulation_rules->{no_auto_renewal_after_hard_limit}
3249              or $circulation_rules->{no_auto_renewal_after_hard_limit} eq '' );
3250
3251     my $maximum_renewal_date;
3252     if ( $circulation_rules->{no_auto_renewal_after} ) {
3253         $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3254         $maximum_renewal_date->add(
3255             $circulation_rules->{lengthunit} => $circulation_rules->{no_auto_renewal_after}
3256         );
3257     }
3258
3259     if ( $circulation_rules->{no_auto_renewal_after_hard_limit} ) {
3260         my $dt = dt_from_string( $circulation_rules->{no_auto_renewal_after_hard_limit} );
3261         $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3262     }
3263     return $maximum_renewal_date;
3264 }
3265
3266
3267 =head2 GetIssuingCharges
3268
3269   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3270
3271 Calculate how much it would cost for a given patron to borrow a given
3272 item, including any applicable discounts.
3273
3274 C<$itemnumber> is the item number of item the patron wishes to borrow.
3275
3276 C<$borrowernumber> is the patron's borrower number.
3277
3278 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3279 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3280 if it's a video).
3281
3282 =cut
3283
3284 sub GetIssuingCharges {
3285
3286     # calculate charges due
3287     my ( $itemnumber, $borrowernumber ) = @_;
3288     my $charge = 0;
3289     my $dbh    = C4::Context->dbh;
3290     my $item_type;
3291
3292     # Get the book's item type and rental charge (via its biblioitem).
3293     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3294         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3295     $charge_query .= (C4::Context->preference('item-level_itypes'))
3296         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3297         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3298
3299     $charge_query .= ' WHERE items.itemnumber =?';
3300
3301     my $sth = $dbh->prepare($charge_query);
3302     $sth->execute($itemnumber);
3303     if ( my $item_data = $sth->fetchrow_hashref ) {
3304         $item_type = $item_data->{itemtype};
3305         $charge    = $item_data->{rentalcharge};
3306         my $branch = C4::Context::mybranch();
3307         my $patron = Koha::Patrons->find( $borrowernumber );
3308         my $discount = _get_discount_from_rule($patron->categorycode, $branch, $item_type);
3309         if ($discount) {
3310             # We may have multiple rules so get the most specific
3311             $charge = ( $charge * ( 100 - $discount ) ) / 100;
3312         }
3313         if ($charge) {
3314             $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3315         }
3316     }
3317
3318     return ( $charge, $item_type );
3319 }
3320
3321 # Select most appropriate discount rule from those returned
3322 sub _get_discount_from_rule {
3323     my ($categorycode, $branchcode, $itemtype) = @_;
3324
3325     # Set search precedences
3326     my @params = (
3327         {
3328             branchcode   => $branchcode,
3329             itemtype     => $itemtype,
3330             categorycode => $categorycode,
3331         },
3332         {
3333             branchcode   => undef,
3334             categorycode => $categorycode,
3335             itemtype     => $itemtype,
3336         },
3337         {
3338             branchcode   => $branchcode,
3339             categorycode => $categorycode,
3340             itemtype     => undef,
3341         },
3342         {
3343             branchcode   => undef,
3344             categorycode => $categorycode,
3345             itemtype     => undef,
3346         },
3347     );
3348
3349     foreach my $params (@params) {
3350         my $rule = Koha::CirculationRules->search(
3351             {
3352                 rule_name => 'rentaldiscount',
3353                 %$params,
3354             }
3355         )->next();
3356
3357         return $rule->rule_value if $rule;
3358     }
3359
3360     # none of the above
3361     return 0;
3362 }
3363
3364 =head2 AddIssuingCharge
3365
3366   &AddIssuingCharge( $checkout, $charge, $type )
3367
3368 =cut
3369
3370 sub AddIssuingCharge {
3371     my ( $checkout, $charge, $type ) = @_;
3372
3373     # FIXME What if checkout does not exist?
3374
3375     my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3376     my $accountline = $account->add_debit(
3377         {
3378             amount      => $charge,
3379             note        => undef,
3380             user_id     => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
3381             library_id  => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3382             interface   => C4::Context->interface,
3383             type        => $type,
3384             item_id     => $checkout->itemnumber,
3385             issue_id    => $checkout->issue_id,
3386         }
3387     );
3388 }
3389
3390 =head2 GetTransfers
3391
3392   GetTransfers($itemnumber);
3393
3394 =cut
3395
3396 sub GetTransfers {
3397     my ($itemnumber) = @_;
3398
3399     my $dbh = C4::Context->dbh;
3400
3401     my $query = '
3402         SELECT datesent,
3403                frombranch,
3404                tobranch,
3405                branchtransfer_id
3406         FROM branchtransfers
3407         WHERE itemnumber = ?
3408           AND datearrived IS NULL
3409         ';
3410     my $sth = $dbh->prepare($query);
3411     $sth->execute($itemnumber);
3412     my @row = $sth->fetchrow_array();
3413     return @row;
3414 }
3415
3416 =head2 GetTransfersFromTo
3417
3418   @results = GetTransfersFromTo($frombranch,$tobranch);
3419
3420 Returns the list of pending transfers between $from and $to branch
3421
3422 =cut
3423
3424 sub GetTransfersFromTo {
3425     my ( $frombranch, $tobranch ) = @_;
3426     return unless ( $frombranch && $tobranch );
3427     my $dbh   = C4::Context->dbh;
3428     my $query = "
3429         SELECT branchtransfer_id,itemnumber,datesent,frombranch
3430         FROM   branchtransfers
3431         WHERE  frombranch=?
3432           AND  tobranch=?
3433           AND datearrived IS NULL
3434     ";
3435     my $sth = $dbh->prepare($query);
3436     $sth->execute( $frombranch, $tobranch );
3437     my @gettransfers;
3438
3439     while ( my $data = $sth->fetchrow_hashref ) {
3440         push @gettransfers, $data;
3441     }
3442     return (@gettransfers);
3443 }
3444
3445 =head2 DeleteTransfer
3446
3447   &DeleteTransfer($itemnumber);
3448
3449 =cut
3450
3451 sub DeleteTransfer {
3452     my ($itemnumber) = @_;
3453     return unless $itemnumber;
3454     my $dbh          = C4::Context->dbh;
3455     my $sth          = $dbh->prepare(
3456         "DELETE FROM branchtransfers
3457          WHERE itemnumber=?
3458          AND datearrived IS NULL "
3459     );
3460     return $sth->execute($itemnumber);
3461 }
3462
3463 =head2 SendCirculationAlert
3464
3465 Send out a C<check-in> or C<checkout> alert using the messaging system.
3466
3467 B<Parameters>:
3468
3469 =over 4
3470
3471 =item type
3472
3473 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3474
3475 =item item
3476
3477 Hashref of information about the item being checked in or out.
3478
3479 =item borrower
3480
3481 Hashref of information about the borrower of the item.
3482
3483 =item branch
3484
3485 The branchcode from where the checkout or check-in took place.
3486
3487 =back
3488
3489 B<Example>:
3490
3491     SendCirculationAlert({
3492         type     => 'CHECKOUT',
3493         item     => $item,
3494         borrower => $borrower,
3495         branch   => $branch,
3496     });
3497
3498 =cut
3499
3500 sub SendCirculationAlert {
3501     my ($opts) = @_;
3502     my ($type, $item, $borrower, $branch) =
3503         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3504     my %message_name = (
3505         CHECKIN  => 'Item_Check_in',
3506         CHECKOUT => 'Item_Checkout',
3507         RENEWAL  => 'Item_Checkout',
3508     );
3509     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3510         borrowernumber => $borrower->{borrowernumber},
3511         message_name   => $message_name{$type},
3512     });
3513     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3514
3515     my $schema = Koha::Database->new->schema;
3516     my @transports = keys %{ $borrower_preferences->{transports} };
3517
3518     # From the MySQL doc:
3519     # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3520     # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3521     # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3522     my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_TESTING};
3523
3524     for my $mtt (@transports) {
3525         my $letter =  C4::Letters::GetPreparedLetter (
3526             module => 'circulation',
3527             letter_code => $type,
3528             branchcode => $branch,
3529             message_transport_type => $mtt,
3530             lang => $borrower->{lang},
3531             tables => {
3532                 $issues_table => $item->{itemnumber},
3533                 'items'       => $item->{itemnumber},
3534                 'biblio'      => $item->{biblionumber},
3535                 'biblioitems' => $item->{biblionumber},
3536                 'borrowers'   => $borrower,
3537                 'branches'    => $branch,
3538             }
3539         ) or next;
3540
3541         $schema->storage->txn_begin;
3542         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3543         C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3544         my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3545         unless ( $message ) {
3546             C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3547             C4::Message->enqueue($letter, $borrower, $mtt);
3548         } else {
3549             $message->append($letter);
3550             $message->update;
3551         }
3552         C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3553         $schema->storage->txn_commit;
3554     }
3555
3556     return;
3557 }
3558
3559 =head2 updateWrongTransfer
3560
3561   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3562
3563 This function validate the line of brachtransfer but with the wrong destination (mistake from a librarian ...), and create a new line in branchtransfer from the actual library to the original library of reservation 
3564
3565 =cut
3566
3567 sub updateWrongTransfer {
3568         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3569         my $dbh = C4::Context->dbh;     
3570 # first step validate the actual line of transfert .
3571         my $sth =
3572                 $dbh->prepare(
3573                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3574                 );
3575                 $sth->execute($FromLibrary,$itemNumber);
3576
3577 # second step create a new line of branchtransfer to the right location .
3578         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3579
3580 #third step changing holdingbranch of item
3581     my $item = Koha::Items->find($itemNumber)->holdingbranch($FromLibrary)->store;
3582 }
3583
3584 =head2 CalcDateDue
3585
3586 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3587
3588 this function calculates the due date given the start date and configured circulation rules,
3589 checking against the holidays calendar as per the 'useDaysMode' syspref.
3590 C<$startdate>   = DateTime object representing start date of loan period (assumed to be today)
3591 C<$itemtype>  = itemtype code of item in question
3592 C<$branch>  = location whose calendar to use
3593 C<$borrower> = Borrower object
3594 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3595
3596 =cut
3597
3598 sub CalcDateDue {
3599     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3600
3601     $isrenewal ||= 0;
3602
3603     # loanlength now a href
3604     my $loanlength =
3605             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3606
3607     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3608             ? qq{renewalperiod}
3609             : qq{issuelength};
3610
3611     my $datedue;
3612     if ( $startdate ) {
3613         if (ref $startdate ne 'DateTime' ) {
3614             $datedue = dt_from_string($datedue);
3615         } else {
3616             $datedue = $startdate->clone;
3617         }
3618     } else {
3619         $datedue = dt_from_string()->truncate( to => 'minute' );
3620     }
3621
3622
3623     # calculate the datedue as normal
3624     if ( C4::Context->preference('useDaysMode') eq 'Days' )
3625     {    # ignoring calendar
3626         if ( $loanlength->{lengthunit} eq 'hours' ) {
3627             $datedue->add( hours => $loanlength->{$length_key} );
3628         } else {    # days
3629             $datedue->add( days => $loanlength->{$length_key} );
3630             $datedue->set_hour(23);
3631             $datedue->set_minute(59);
3632         }
3633     } else {
3634         my $dur;
3635         if ($loanlength->{lengthunit} eq 'hours') {
3636             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3637         }
3638         else { # days
3639             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3640         }
3641         my $calendar = Koha::Calendar->new( branchcode => $branch );
3642         $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3643         if ($loanlength->{lengthunit} eq 'days') {
3644             $datedue->set_hour(23);
3645             $datedue->set_minute(59);
3646         }
3647     }
3648
3649     # if Hard Due Dates are used, retrieve them and apply as necessary
3650     my ( $hardduedate, $hardduedatecompare ) =
3651       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3652     if ($hardduedate) {    # hardduedates are currently dates
3653         $hardduedate->truncate( to => 'minute' );
3654         $hardduedate->set_hour(23);
3655         $hardduedate->set_minute(59);
3656         my $cmp = DateTime->compare( $hardduedate, $datedue );
3657
3658 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3659 # if the calculated date is before the 'after' Hard Due Date (floor), override
3660 # if the hard due date is set to 'exactly', overrride
3661         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3662             $datedue = $hardduedate->clone;
3663         }
3664
3665         # in all other cases, keep the date due as it is
3666
3667     }
3668
3669     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3670     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3671         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3672         if( $expiry_dt ) { #skip empty expiry date..
3673             $expiry_dt->set( hour => 23, minute => 59);
3674             my $d1= $datedue->clone->set_time_zone('floating');
3675             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3676                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3677             }
3678         }
3679         if ( C4::Context->preference('useDaysMode') ne 'Days' ) {
3680           my $calendar = Koha::Calendar->new( branchcode => $branch );
3681           if ( $calendar->is_holiday($datedue) ) {
3682               # Don't return on a closed day
3683               $datedue = $calendar->prev_open_days( $datedue, 1 );
3684           }
3685         }
3686     }
3687
3688     return $datedue;
3689 }
3690
3691
3692 sub CheckValidBarcode{
3693 my ($barcode) = @_;
3694 my $dbh = C4::Context->dbh;
3695 my $query=qq|SELECT count(*) 
3696              FROM items 
3697              WHERE barcode=?
3698             |;
3699 my $sth = $dbh->prepare($query);
3700 $sth->execute($barcode);
3701 my $exist=$sth->fetchrow ;
3702 return $exist;
3703 }
3704
3705 =head2 IsBranchTransferAllowed
3706
3707   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3708
3709 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3710
3711 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3712 Koha::Item->can_be_transferred.
3713
3714 =cut
3715
3716 sub IsBranchTransferAllowed {
3717         my ( $toBranch, $fromBranch, $code ) = @_;
3718
3719         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3720         
3721         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3722         my $dbh = C4::Context->dbh;
3723             
3724         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3725         $sth->execute( $toBranch, $fromBranch, $code );
3726         my $limit = $sth->fetchrow_hashref();
3727                         
3728         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3729         if ( $limit->{'limitId'} ) {
3730                 return 0;
3731         } else {
3732                 return 1;
3733         }
3734 }                                                        
3735
3736 =head2 CreateBranchTransferLimit
3737
3738   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3739
3740 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3741
3742 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3743
3744 =cut
3745
3746 sub CreateBranchTransferLimit {
3747    my ( $toBranch, $fromBranch, $code ) = @_;
3748    return unless defined($toBranch) && defined($fromBranch);
3749    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3750    
3751    my $dbh = C4::Context->dbh;
3752    
3753    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3754    return $sth->execute( $code, $toBranch, $fromBranch );
3755 }
3756
3757 =head2 DeleteBranchTransferLimits
3758
3759     my $result = DeleteBranchTransferLimits($frombranch);
3760
3761 Deletes all the library transfer limits for one library.  Returns the
3762 number of limits deleted, 0e0 if no limits were deleted, or undef if
3763 no arguments are supplied.
3764
3765 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3766     fromBranch => $fromBranch
3767     })->delete.
3768
3769 =cut
3770
3771 sub DeleteBranchTransferLimits {
3772     my $branch = shift;
3773     return unless defined $branch;
3774     my $dbh    = C4::Context->dbh;
3775     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3776     return $sth->execute($branch);
3777 }
3778
3779 sub ReturnLostItem{
3780     my ( $borrowernumber, $itemnum ) = @_;
3781     MarkIssueReturned( $borrowernumber, $itemnum );
3782 }
3783
3784
3785 sub LostItem{
3786     my ($itemnumber, $mark_lost_from, $force_mark_returned) = @_;
3787
3788     unless ( $mark_lost_from ) {
3789         # Temporary check to avoid regressions
3790         die q|LostItem called without $mark_lost_from, check the API.|;
3791     }
3792
3793     my $mark_returned;
3794     if ( $force_mark_returned ) {
3795         $mark_returned = 1;
3796     } else {
3797         my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3798         $mark_returned = ( $pref =~ m|$mark_lost_from| );
3799     }
3800
3801     my $dbh = C4::Context->dbh();
3802     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3803                            FROM issues 
3804                            JOIN items USING (itemnumber) 
3805                            JOIN biblio USING (biblionumber)
3806                            WHERE issues.itemnumber=?");
3807     $sth->execute($itemnumber);
3808     my $issues=$sth->fetchrow_hashref();
3809
3810     # If a borrower lost the item, add a replacement cost to the their record
3811     if ( my $borrowernumber = $issues->{borrowernumber} ){
3812         my $patron = Koha::Patrons->find( $borrowernumber );
3813
3814         my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 'LOST');
3815         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3816
3817         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3818             C4::Accounts::chargelostitem(
3819                 $borrowernumber,
3820                 $itemnumber,
3821                 $issues->{'replacementprice'},
3822                 sprintf( "%s %s %s",
3823                     $issues->{'title'}          || q{},
3824                     $issues->{'barcode'}        || q{},
3825                     $issues->{'itemcallnumber'} || q{},
3826                 ),
3827             );
3828             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3829             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3830         }
3831
3832         MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned;
3833     }
3834
3835     #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch)
3836     if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) {
3837         Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store;
3838     }
3839     my $transferdeleted = DeleteTransfer($itemnumber);
3840 }
3841
3842 sub GetOfflineOperations {
3843     my $dbh = C4::Context->dbh;
3844     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3845     $sth->execute(C4::Context->userenv->{'branch'});
3846     my $results = $sth->fetchall_arrayref({});
3847     return $results;
3848 }
3849
3850 sub GetOfflineOperation {
3851     my $operationid = shift;
3852     return unless $operationid;
3853     my $dbh = C4::Context->dbh;
3854     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3855     $sth->execute( $operationid );
3856     return $sth->fetchrow_hashref;
3857 }
3858
3859 sub AddOfflineOperation {
3860     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3861     my $dbh = C4::Context->dbh;
3862     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3863     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3864     return "Added.";
3865 }
3866
3867 sub DeleteOfflineOperation {
3868     my $dbh = C4::Context->dbh;
3869     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3870     $sth->execute( shift );
3871     return "Deleted.";
3872 }
3873
3874 sub ProcessOfflineOperation {
3875     my $operation = shift;
3876
3877     my $report;
3878     if ( $operation->{action} eq 'return' ) {
3879         $report = ProcessOfflineReturn( $operation );
3880     } elsif ( $operation->{action} eq 'issue' ) {
3881         $report = ProcessOfflineIssue( $operation );
3882     } elsif ( $operation->{action} eq 'payment' ) {
3883         $report = ProcessOfflinePayment( $operation );
3884     }
3885
3886     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3887
3888     return $report;
3889 }
3890
3891 sub ProcessOfflineReturn {
3892     my $operation = shift;
3893
3894     my $item = Koha::Items->find({barcode => $operation->{barcode}});
3895
3896     if ( $item ) {
3897         my $itemnumber = $item->itemnumber;
3898         my $issue = GetOpenIssue( $itemnumber );
3899         if ( $issue ) {
3900             my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
3901             ModDateLastSeen( $itemnumber, $leave_item_lost );
3902             MarkIssueReturned(
3903                 $issue->{borrowernumber},
3904                 $itemnumber,
3905                 $operation->{timestamp},
3906             );
3907             $item->renewals(0);
3908             $item->onloan(undef);
3909             $item->store({ log_action => 0 });
3910             return "Success.";
3911         } else {
3912             return "Item not issued.";
3913         }
3914     } else {
3915         return "Item not found.";
3916     }
3917 }
3918
3919 sub ProcessOfflineIssue {
3920     my $operation = shift;
3921
3922     my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3923
3924     if ( $patron ) {
3925         my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3926         unless ($item) {
3927             return "Barcode not found.";
3928         }
3929         my $itemnumber = $item->itemnumber;
3930         my $issue = GetOpenIssue( $itemnumber );
3931
3932         if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3933             MarkIssueReturned(
3934                 $issue->{borrowernumber},
3935                 $itemnumber,
3936                 $operation->{timestamp},
3937             );
3938         }
3939         AddIssue(
3940             $patron->unblessed,
3941             $operation->{'barcode'},
3942             undef,
3943             1,
3944             $operation->{timestamp},
3945             undef,
3946         );
3947         return "Success.";
3948     } else {
3949         return "Borrower not found.";
3950     }
3951 }
3952
3953 sub ProcessOfflinePayment {
3954     my $operation = shift;
3955
3956     my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
3957
3958     $patron->account->pay(
3959         {
3960             amount     => $operation->{amount},
3961             library_id => $operation->{branchcode},
3962             interface  => 'koc'
3963         }
3964     );
3965
3966     return "Success.";
3967 }
3968
3969 =head2 TransferSlip
3970
3971   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3972
3973   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3974
3975 =cut
3976
3977 sub TransferSlip {
3978     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3979
3980     my $item =
3981       $itemnumber
3982       ? Koha::Items->find($itemnumber)
3983       : Koha::Items->find( { barcode => $barcode } );
3984
3985     $item or return;
3986
3987     return C4::Letters::GetPreparedLetter (
3988         module => 'circulation',
3989         letter_code => 'TRANSFERSLIP',
3990         branchcode => $branch,
3991         tables => {
3992             'branches'    => $to_branch,
3993             'biblio'      => $item->biblionumber,
3994             'items'       => $item->unblessed,
3995         },
3996     );
3997 }
3998
3999 =head2 CheckIfIssuedToPatron
4000
4001   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
4002
4003   Return 1 if any record item is issued to patron, otherwise return 0
4004
4005 =cut
4006
4007 sub CheckIfIssuedToPatron {
4008     my ($borrowernumber, $biblionumber) = @_;
4009
4010     my $dbh = C4::Context->dbh;
4011     my $query = q|
4012         SELECT COUNT(*) FROM issues
4013         LEFT JOIN items ON items.itemnumber = issues.itemnumber
4014         WHERE items.biblionumber = ?
4015         AND issues.borrowernumber = ?
4016     |;
4017     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
4018     return 1 if $is_issued;
4019     return;
4020 }
4021
4022 =head2 IsItemIssued
4023
4024   IsItemIssued( $itemnumber )
4025
4026   Return 1 if the item is on loan, otherwise return 0
4027
4028 =cut
4029
4030 sub IsItemIssued {
4031     my $itemnumber = shift;
4032     my $dbh = C4::Context->dbh;
4033     my $sth = $dbh->prepare(q{
4034         SELECT COUNT(*)
4035         FROM issues
4036         WHERE itemnumber = ?
4037     });
4038     $sth->execute($itemnumber);
4039     return $sth->fetchrow;
4040 }
4041
4042 =head2 GetAgeRestriction
4043
4044   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
4045   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
4046
4047   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
4048   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
4049
4050 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
4051 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
4052 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
4053          Negative days mean the borrower has gone past the age restriction age.
4054
4055 =cut
4056
4057 sub GetAgeRestriction {
4058     my ($record_restrictions, $borrower) = @_;
4059     my $markers = C4::Context->preference('AgeRestrictionMarker');
4060
4061     return unless $record_restrictions;
4062     # Split $record_restrictions to something like FSK 16 or PEGI 6
4063     my @values = split ' ', uc($record_restrictions);
4064     return unless @values;
4065
4066     # Search first occurrence of one of the markers
4067     my @markers = split /\|/, uc($markers);
4068     return unless @markers;
4069
4070     my $index            = 0;
4071     my $restriction_year = 0;
4072     for my $value (@values) {
4073         $index++;
4074         for my $marker (@markers) {
4075             $marker =~ s/^\s+//;    #remove leading spaces
4076             $marker =~ s/\s+$//;    #remove trailing spaces
4077             if ( $marker eq $value ) {
4078                 if ( $index <= $#values ) {
4079                     $restriction_year += $values[$index];
4080                 }
4081                 last;
4082             }
4083             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
4084
4085                 # Perhaps it is something like "K16" (as in Finland)
4086                 $restriction_year += $1;
4087                 last;
4088             }
4089         }
4090         last if ( $restriction_year > 0 );
4091     }
4092
4093     #Check if the borrower is age restricted for this material and for how long.
4094     if ($restriction_year && $borrower) {
4095         if ( $borrower->{'dateofbirth'} ) {
4096             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
4097             $alloweddate[0] += $restriction_year;
4098
4099             #Prevent runime eror on leap year (invalid date)
4100             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
4101                 $alloweddate[2] = 28;
4102             }
4103
4104             #Get how many days the borrower has to reach the age restriction
4105             my @Today = split /-/, dt_from_string()->ymd();
4106             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
4107             #Negative days means the borrower went past the age restriction age
4108             return ($restriction_year, $daysToAgeRestriction);
4109         }
4110     }
4111
4112     return ($restriction_year);
4113 }
4114
4115
4116 =head2 GetPendingOnSiteCheckouts
4117
4118 =cut
4119
4120 sub GetPendingOnSiteCheckouts {
4121     my $dbh = C4::Context->dbh;
4122     return $dbh->selectall_arrayref(q|
4123         SELECT
4124           items.barcode,
4125           items.biblionumber,
4126           items.itemnumber,
4127           items.itemnotes,
4128           items.itemcallnumber,
4129           items.location,
4130           issues.date_due,
4131           issues.branchcode,
4132           issues.date_due < NOW() AS is_overdue,
4133           biblio.author,
4134           biblio.title,
4135           borrowers.firstname,
4136           borrowers.surname,
4137           borrowers.cardnumber,
4138           borrowers.borrowernumber
4139         FROM items
4140         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4141         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4142         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4143         WHERE issues.onsite_checkout = 1
4144     |, { Slice => {} } );
4145 }
4146
4147 sub GetTopIssues {
4148     my ($params) = @_;
4149
4150     my ($count, $branch, $itemtype, $ccode, $newness)
4151         = @$params{qw(count branch itemtype ccode newness)};
4152
4153     my $dbh = C4::Context->dbh;
4154     my $query = q{
4155         SELECT * FROM (
4156         SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4157           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4158           i.ccode, SUM(i.issues) AS count
4159         FROM biblio b
4160         LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4161         LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4162     };
4163
4164     my (@where_strs, @where_args);
4165
4166     if ($branch) {
4167         push @where_strs, 'i.homebranch = ?';
4168         push @where_args, $branch;
4169     }
4170     if ($itemtype) {
4171         if (C4::Context->preference('item-level_itypes')){
4172             push @where_strs, 'i.itype = ?';
4173             push @where_args, $itemtype;
4174         } else {
4175             push @where_strs, 'bi.itemtype = ?';
4176             push @where_args, $itemtype;
4177         }
4178     }
4179     if ($ccode) {
4180         push @where_strs, 'i.ccode = ?';
4181         push @where_args, $ccode;
4182     }
4183     if ($newness) {
4184         push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4185         push @where_args, $newness;
4186     }
4187
4188     if (@where_strs) {
4189         $query .= 'WHERE ' . join(' AND ', @where_strs);
4190     }
4191
4192     $query .= q{
4193         GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4194           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4195           i.ccode
4196         ORDER BY count DESC
4197     };
4198
4199     $query .= q{ ) xxx WHERE count > 0 };
4200     $count = int($count);
4201     if ($count > 0) {
4202         $query .= "LIMIT $count";
4203     }
4204
4205     my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4206
4207     return @$rows;
4208 }
4209
4210 sub _CalculateAndUpdateFine {
4211     my ($params) = @_;
4212
4213     my $borrower    = $params->{borrower};
4214     my $item        = $params->{item};
4215     my $issue       = $params->{issue};
4216     my $return_date = $params->{return_date};
4217
4218     unless ($borrower) { carp "No borrower passed in!" && return; }
4219     unless ($item)     { carp "No item passed in!"     && return; }
4220     unless ($issue)    { carp "No issue passed in!"    && return; }
4221
4222     my $datedue = dt_from_string( $issue->date_due );
4223
4224     # we only need to calculate and change the fines if we want to do that on return
4225     # Should be on for hourly loans
4226     my $control = C4::Context->preference('CircControl');
4227     my $control_branchcode =
4228         ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4229       : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
4230       :                                     $issue->branchcode;
4231
4232     my $date_returned = $return_date ? $return_date : dt_from_string();
4233
4234     my ( $amount, $unitcounttotal, $unitcount  ) =
4235       C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4236
4237     if ( C4::Context->preference('finesMode') eq 'production' ) {
4238         if ( $amount > 0 ) {
4239             C4::Overdues::UpdateFine({
4240                 issue_id       => $issue->issue_id,
4241                 itemnumber     => $issue->itemnumber,
4242                 borrowernumber => $issue->borrowernumber,
4243                 amount         => $amount,
4244                 due            => output_pref($datedue),
4245             });
4246         }
4247         elsif ($return_date) {
4248
4249             # Backdated returns may have fines that shouldn't exist,
4250             # so in this case, we need to drop those fines to 0
4251
4252             C4::Overdues::UpdateFine({
4253                 issue_id       => $issue->issue_id,
4254                 itemnumber     => $issue->itemnumber,
4255                 borrowernumber => $issue->borrowernumber,
4256                 amount         => 0,
4257                 due            => output_pref($datedue),
4258             });
4259         }
4260     }
4261 }
4262
4263 sub _item_denied_renewal {
4264     my ($params) = @_;
4265
4266     my $item = $params->{item};
4267     return unless $item;
4268
4269     my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4270     return unless $denyingrules;
4271     foreach my $field (keys %$denyingrules) {
4272         my $val = $item->$field;
4273         if( !defined $val) {
4274             if ( any { !defined $_ }  @{$denyingrules->{$field}} ){
4275                 return 1;
4276             }
4277         } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4278            # If the results matches the values in the syspref
4279            # We return true if match found
4280             return 1;
4281         }
4282     }
4283     return 0;
4284 }
4285
4286
4287 1;
4288
4289 __END__
4290
4291 =head1 AUTHOR
4292
4293 Koha Development Team <http://koha-community.org/>
4294
4295 =cut