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