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