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