Bug 14591: book drop / drop box mode incorrectly decrements accrued overdue fines
[koha.git] / C4 / Circulation.pm
1 package C4::Circulation;
2
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use DateTime;
25 use POSIX qw( floor );
26 use Koha::DateUtils;
27 use C4::Context;
28 use C4::Stats;
29 use C4::Reserves;
30 use C4::Biblio;
31 use C4::Items;
32 use C4::Members;
33 use C4::Accounts;
34 use C4::ItemCirculationAlertPreference;
35 use C4::Message;
36 use C4::Debug;
37 use C4::Log; # logaction
38 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
39 use C4::RotatingCollections qw(GetCollectionItemBranches);
40 use Algorithm::CheckDigits;
41
42 use Data::Dumper;
43 use Koha::Account;
44 use Koha::AuthorisedValues;
45 use Koha::Biblioitems;
46 use Koha::DateUtils;
47 use Koha::Calendar;
48 use Koha::Checkouts;
49 use Koha::IssuingRules;
50 use Koha::Items;
51 use Koha::Patrons;
52 use Koha::Patron::Debarments;
53 use Koha::Database;
54 use Koha::Libraries;
55 use Koha::Account::Lines;
56 use Koha::Holds;
57 use Koha::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     my $dbh = C4::Context->dbh;
1538     my $sth = $dbh->prepare(qq{
1539         SELECT issuelength, lengthunit, renewalperiod
1540         FROM issuingrules
1541         WHERE   categorycode=?
1542             AND itemtype=?
1543             AND branchcode=?
1544             AND issuelength IS NOT NULL
1545     });
1546
1547     # try to find issuelength & return the 1st available.
1548     # check with borrowertype, itemtype and branchcode, then without one of those parameters
1549     $sth->execute( $borrowertype, $itemtype, $branchcode );
1550     my $loanlength = $sth->fetchrow_hashref;
1551
1552     return $loanlength
1553       if defined($loanlength) && defined $loanlength->{issuelength};
1554
1555     $sth->execute( $borrowertype, '*', $branchcode );
1556     $loanlength = $sth->fetchrow_hashref;
1557     return $loanlength
1558       if defined($loanlength) && defined $loanlength->{issuelength};
1559
1560     $sth->execute( '*', $itemtype, $branchcode );
1561     $loanlength = $sth->fetchrow_hashref;
1562     return $loanlength
1563       if defined($loanlength) && defined $loanlength->{issuelength};
1564
1565     $sth->execute( '*', '*', $branchcode );
1566     $loanlength = $sth->fetchrow_hashref;
1567     return $loanlength
1568       if defined($loanlength) && defined $loanlength->{issuelength};
1569
1570     $sth->execute( $borrowertype, $itemtype, '*' );
1571     $loanlength = $sth->fetchrow_hashref;
1572     return $loanlength
1573       if defined($loanlength) && defined $loanlength->{issuelength};
1574
1575     $sth->execute( $borrowertype, '*', '*' );
1576     $loanlength = $sth->fetchrow_hashref;
1577     return $loanlength
1578       if defined($loanlength) && defined $loanlength->{issuelength};
1579
1580     $sth->execute( '*', $itemtype, '*' );
1581     $loanlength = $sth->fetchrow_hashref;
1582     return $loanlength
1583       if defined($loanlength) && defined $loanlength->{issuelength};
1584
1585     $sth->execute( '*', '*', '*' );
1586     $loanlength = $sth->fetchrow_hashref;
1587     return $loanlength
1588       if defined($loanlength) && defined $loanlength->{issuelength};
1589
1590     # if no rule is set => 0 day (hardcoded)
1591     return {
1592         issuelength => 0,
1593         renewalperiod => 0,
1594         lengthunit => 'days',
1595     };
1596
1597 }
1598
1599
1600 =head2 GetHardDueDate
1601
1602   my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1603
1604 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1605
1606 =cut
1607
1608 sub GetHardDueDate {
1609     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1610
1611     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1612         {   categorycode => $borrowertype,
1613             itemtype     => $itemtype,
1614             branchcode   => $branchcode
1615         }
1616     );
1617
1618
1619     if ( defined( $issuing_rule ) ) {
1620         if ( $issuing_rule->hardduedate ) {
1621             return (dt_from_string($issuing_rule->hardduedate, 'iso'),$issuing_rule->hardduedatecompare);
1622         } else {
1623             return (undef, undef);
1624         }
1625     }
1626 }
1627
1628 =head2 GetBranchBorrowerCircRule
1629
1630   my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1631
1632 Retrieves circulation rule attributes that apply to the given
1633 branch and patron category, regardless of item type.  
1634 The return value is a hashref containing the following key:
1635
1636 patron_maxissueqty - maximum number of loans that a
1637 patron of the given category can have at the given
1638 branch.  If the value is undef, no limit.
1639
1640 patron_maxonsiteissueqty - maximum of on-site checkouts that a
1641 patron of the given category can have at the given
1642 branch.  If the value is undef, no limit.
1643
1644 This will check for different branch/category combinations in the following order:
1645 branch and category
1646 branch only
1647 category only
1648 default branch and category
1649
1650 If no rule has been found in the database, it will default to
1651 the buillt in rule:
1652
1653 patron_maxissueqty - undef
1654 patron_maxonsiteissueqty - undef
1655
1656 C<$branchcode> and C<$categorycode> should contain the
1657 literal branch code and patron category code, respectively - no
1658 wildcards.
1659
1660 =cut
1661
1662 sub GetBranchBorrowerCircRule {
1663     my ( $branchcode, $categorycode ) = @_;
1664
1665     # Initialize default values
1666     my $rules = {
1667         patron_maxissueqty       => undef,
1668         patron_maxonsiteissueqty => undef,
1669     };
1670
1671     # Search for rules!
1672     foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
1673         my $rule = Koha::CirculationRules->get_effective_rule(
1674             {
1675                 categorycode => $categorycode,
1676                 itemtype     => undef,
1677                 branchcode   => $branchcode,
1678                 rule_name    => $rule_name,
1679             }
1680         );
1681
1682         $rules->{$rule_name} = $rule->rule_value if defined $rule;
1683     }
1684
1685     return $rules;
1686 }
1687
1688 =head2 GetBranchItemRule
1689
1690   my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1691
1692 Retrieves circulation rule attributes that apply to the given
1693 branch and item type, regardless of patron category.
1694
1695 The return value is a hashref containing the following keys:
1696
1697 holdallowed => Hold policy for this branch and itemtype. Possible values:
1698   0: No holds allowed.
1699   1: Holds allowed only by patrons that have the same homebranch as the item.
1700   2: Holds allowed from any patron.
1701
1702 returnbranch => branch to which to return item.  Possible values:
1703   noreturn: do not return, let item remain where checked in (floating collections)
1704   homebranch: return to item's home branch
1705   holdingbranch: return to issuer branch
1706
1707 This searches branchitemrules in the following order:
1708
1709   * Same branchcode and itemtype
1710   * Same branchcode, itemtype '*'
1711   * branchcode '*', same itemtype
1712   * branchcode and itemtype '*'
1713
1714 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1715
1716 =cut
1717
1718 sub GetBranchItemRule {
1719     my ( $branchcode, $itemtype ) = @_;
1720     my $dbh = C4::Context->dbh();
1721     my $result = {};
1722
1723     my @attempts = (
1724         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1725             FROM branch_item_rules
1726             WHERE branchcode = ?
1727               AND itemtype = ?', $branchcode, $itemtype],
1728         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1729             FROM default_branch_circ_rules
1730             WHERE branchcode = ?', $branchcode],
1731         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1732             FROM default_branch_item_rules
1733             WHERE itemtype = ?', $itemtype],
1734         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1735             FROM default_circ_rules'],
1736     );
1737
1738     foreach my $attempt (@attempts) {
1739         my ($query, @bind_params) = @{$attempt};
1740         my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1741           or next;
1742
1743         # Since branch/category and branch/itemtype use the same per-branch
1744         # defaults tables, we have to check that the key we want is set, not
1745         # just that a row was returned
1746         $result->{'holdallowed'}  = $search_result->{'holdallowed'}  unless ( defined $result->{'holdallowed'} );
1747         $result->{'hold_fulfillment_policy'} = $search_result->{'hold_fulfillment_policy'} unless ( defined $result->{'hold_fulfillment_policy'} );
1748         $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1749     }
1750     
1751     # built-in default circulation rule
1752     $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1753     $result->{'hold_fulfillment_policy'} = 'any' unless ( defined $result->{'hold_fulfillment_policy'} );
1754     $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1755
1756     return $result;
1757 }
1758
1759 =head2 AddReturn
1760
1761   ($doreturn, $messages, $iteminformation, $borrower) =
1762       &AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$returndate] );
1763
1764 Returns a book.
1765
1766 =over 4
1767
1768 =item C<$barcode> is the bar code of the book being returned.
1769
1770 =item C<$branch> is the code of the branch where the book is being returned.
1771
1772 =item C<$exemptfine> indicates that overdue charges for the item will be
1773 removed. Optional.
1774
1775 =item C<$dropbox> indicates that the check-in date is assumed to be
1776 yesterday, or the last non-holiday as defined in C4::Calendar .  If
1777 overdue charges are applied and C<$dropbox> is true, the last charge
1778 will be removed.  This assumes that the fines accrual script has run
1779 for _today_. Optional.
1780
1781 =item C<$return_date> allows the default return date to be overridden
1782 by the given return date. Optional.
1783
1784 =back
1785
1786 C<&AddReturn> returns a list of four items:
1787
1788 C<$doreturn> is true iff the return succeeded.
1789
1790 C<$messages> is a reference-to-hash giving feedback on the operation.
1791 The keys of the hash are:
1792
1793 =over 4
1794
1795 =item C<BadBarcode>
1796
1797 No item with this barcode exists. The value is C<$barcode>.
1798
1799 =item C<NotIssued>
1800
1801 The book is not currently on loan. The value is C<$barcode>.
1802
1803 =item C<withdrawn>
1804
1805 This book has been withdrawn/cancelled. The value should be ignored.
1806
1807 =item C<Wrongbranch>
1808
1809 This book has was returned to the wrong branch.  The value is a hashref
1810 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1811 contain the branchcode of the incorrect and correct return library, respectively.
1812
1813 =item C<ResFound>
1814
1815 The item was reserved. The value is a reference-to-hash whose keys are
1816 fields from the reserves table of the Koha database, and
1817 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1818 either C<Waiting>, C<Reserved>, or 0.
1819
1820 =item C<WasReturned>
1821
1822 Value 1 if return is successful.
1823
1824 =item C<NeedsTransfer>
1825
1826 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1827
1828 =back
1829
1830 C<$iteminformation> is a reference-to-hash, giving information about the
1831 returned item from the issues table.
1832
1833 C<$borrower> is a reference-to-hash, giving information about the
1834 patron who last borrowed the book.
1835
1836 =cut
1837
1838 sub AddReturn {
1839     my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
1840
1841     if ($branch and not Koha::Libraries->find($branch)) {
1842         warn "AddReturn error: branch '$branch' not found.  Reverting to " . C4::Context->userenv->{'branch'};
1843         undef $branch;
1844     }
1845     $branch = C4::Context->userenv->{'branch'} unless $branch;  # we trust userenv to be a safe fallback/default
1846     my $messages;
1847     my $patron;
1848     my $doreturn       = 1;
1849     my $validTransfert = 0;
1850     my $stat_type = 'return';
1851
1852     # get information on item
1853     my $item = Koha::Items->find({ barcode => $barcode });
1854     unless ($item) {
1855         return ( 0, { BadBarcode => $barcode } );    # no barcode means no item or borrower.  bail out.
1856     }
1857
1858     my $itemnumber = $item->itemnumber;
1859     my $itemtype = $item->effective_itemtype;
1860
1861     my $issue  = $item->checkout;
1862     if ( $issue ) {
1863         $patron = $issue->patron
1864             or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1865                 . Dumper($issue->unblessed) . "\n";
1866     } else {
1867         $messages->{'NotIssued'} = $barcode;
1868         ModItem({ onloan => undef }, $item->biblionumber, $item->itemnumber) if defined $item->onloan;
1869         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
1870         $doreturn = 0;
1871         # No issue, no borrowernumber.  ONLY if $doreturn, *might* you have a $borrower later.
1872         # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1873         if (C4::Context->preference("RecordLocalUseOnReturn")) {
1874            $messages->{'LocalUse'} = 1;
1875            $stat_type = 'localuse';
1876         }
1877     }
1878
1879     my $item_unblessed = $item->unblessed;
1880     if ( $item->location eq 'PROC' ) {
1881         if ( C4::Context->preference("InProcessingToShelvingCart") ) {
1882             $item_unblessed->{location} = 'CART';
1883         }
1884         else {
1885             $item_unblessed->{location} = $item->permanent_location;
1886         }
1887
1888         ModItem( $item_unblessed, $item->biblionumber, $item->itemnumber, { log_action => 0 } );
1889     }
1890
1891         # full item data, but no borrowernumber or checkout info (no issue)
1892     my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
1893         # get the proper branch to which to return the item
1894     my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
1895         # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1896
1897     my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
1898     my $patron_unblessed = $patron ? $patron->unblessed : {};
1899
1900     my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1901     if ($yaml) {
1902         $yaml = "$yaml\n\n";  # YAML is anal on ending \n. Surplus does not hurt
1903         my $rules;
1904         eval { $rules = YAML::Load($yaml); };
1905         if ($@) {
1906             warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1907         }
1908         else {
1909             foreach my $key ( keys %$rules ) {
1910                 if ( $item->notforloan eq $key ) {
1911                     $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
1912                     ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber, { log_action => 0 } );
1913                     last;
1914                 }
1915             }
1916         }
1917     }
1918
1919     # check if the return is allowed at this branch
1920     my ($returnallowed, $message) = CanBookBeReturned($item_unblessed, $branch);
1921     unless ($returnallowed){
1922         $messages->{'Wrongbranch'} = {
1923             Wrongbranch => $branch,
1924             Rightbranch => $message
1925         };
1926         $doreturn = 0;
1927         return ( $doreturn, $messages, $issue, $patron_unblessed);
1928     }
1929
1930     if ( $item->withdrawn ) { # book has been cancelled
1931         $messages->{'withdrawn'} = 1;
1932         $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1933     }
1934
1935     if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
1936         $doreturn = 0;
1937     }
1938
1939     # case of a return of document (deal with issues and holdingbranch)
1940     my $today = DateTime->now( time_zone => C4::Context->tz() );
1941
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             $today = $return_date if $return_date;
2033             my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item_unblessed, dt_from_string($issue->date_due), $today );
2034             if ($reminder){
2035                 $messages->{'PrevDebarred'} = $debardate;
2036             } else {
2037                 $messages->{'Debarred'} = $debardate if $debardate;
2038             }
2039         # there's no overdue on the item but borrower had been previously debarred
2040         } elsif ( $issue->date_due and $patron->debarred ) {
2041              if ( $patron->debarred eq "9999-12-31") {
2042                 $messages->{'ForeverDebarred'} = $patron->debarred;
2043              } else {
2044                   my $borrower_debar_dt = dt_from_string( $patron->debarred );
2045                   $borrower_debar_dt->truncate(to => 'day');
2046                   my $today_dt = $today->clone()->truncate(to => 'day');
2047                   if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2048                       $messages->{'PrevDebarred'} = $patron->debarred;
2049                   }
2050              }
2051         }
2052     }
2053
2054     # find reserves.....
2055     # if we don't have a reserve with the status W, we launch the Checkreserves routine
2056     my ($resfound, $resrec);
2057     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2058     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2059     if ($resfound) {
2060           $resrec->{'ResFound'} = $resfound;
2061         $messages->{'ResFound'} = $resrec;
2062     }
2063
2064     # Record the fact that this book was returned.
2065     UpdateStats({
2066         branch         => $branch,
2067         type           => $stat_type,
2068         itemnumber     => $itemnumber,
2069         itemtype       => $itemtype,
2070         borrowernumber => $borrowernumber,
2071         ccode          => $item->ccode,
2072     });
2073
2074     # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2075     if ( $patron ) {
2076         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2077         my %conditions = (
2078             branchcode   => $branch,
2079             categorycode => $patron->categorycode,
2080             item_type    => $itemtype,
2081             notification => 'CHECKIN',
2082         );
2083         if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2084             SendCirculationAlert({
2085                 type     => 'CHECKIN',
2086                 item     => $item_unblessed,
2087                 borrower => $patron->unblessed,
2088                 branch   => $branch,
2089             });
2090         }
2091
2092         logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2093             if C4::Context->preference("ReturnLog");
2094         }
2095
2096     # Remove any OVERDUES related debarment if the borrower has no overdues
2097     if ( $borrowernumber
2098       && $patron->debarred
2099       && C4::Context->preference('AutoRemoveOverduesRestrictions')
2100       && !Koha::Patrons->find( $borrowernumber )->has_overdues
2101       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2102     ) {
2103         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2104     }
2105
2106     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2107     if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2108         my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType");
2109         if  (C4::Context->preference("AutomaticItemReturn"    ) or
2110             (C4::Context->preference("UseBranchTransferLimits") and
2111              ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2112            )) {
2113             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->itemnumber,$branch, $returnbranch;
2114             $debug and warn "item: " . Dumper($item_unblessed);
2115             ModItemTransfer($item->itemnumber, $branch, $returnbranch);
2116             $messages->{'WasTransfered'} = 1;
2117         } else {
2118             $messages->{'NeedsTransfer'} = $returnbranch;
2119         }
2120     }
2121
2122     return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2123 }
2124
2125 =head2 MarkIssueReturned
2126
2127   MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy);
2128
2129 Unconditionally marks an issue as being returned by
2130 moving the C<issues> row to C<old_issues> and
2131 setting C<returndate> to the current date.
2132
2133 if C<$returndate> is specified (in iso format), it is used as the date
2134 of the return.
2135
2136 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2137 the old_issue is immediately anonymised
2138
2139 Ideally, this function would be internal to C<C4::Circulation>,
2140 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2141 and offline_circ/process_koc.pl.
2142
2143 =cut
2144
2145 sub MarkIssueReturned {
2146     my ( $borrowernumber, $itemnumber, $returndate, $privacy ) = @_;
2147
2148     # Retrieve the issue
2149     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2150     my $issue_id = $issue->issue_id;
2151
2152     my $anonymouspatron;
2153     if ( $privacy == 2 ) {
2154         # The default of 0 will not work due to foreign key constraints
2155         # The anonymisation will fail if AnonymousPatron is not a valid entry
2156         # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2157         # Note that a warning should appear on the about page (System information tab).
2158         $anonymouspatron = C4::Context->preference('AnonymousPatron');
2159         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."
2160             unless Koha::Patrons->find( $anonymouspatron );
2161     }
2162
2163     my $schema = Koha::Database->schema;
2164
2165     # FIXME Improve the return value and handle it from callers
2166     $schema->txn_do(sub {
2167
2168         # Update the returndate value
2169         if ( $returndate ) {
2170             $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2171         }
2172         else {
2173             $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2174         }
2175
2176         # Create the old_issues entry
2177         my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2178
2179         # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2180         if ( $privacy == 2) {
2181             $old_checkout->borrowernumber($anonymouspatron)->store;
2182         }
2183
2184         # And finally delete the issue
2185         $issue->delete;
2186
2187         ModItem( { 'onloan' => undef }, undef, $itemnumber, { log_action => 0 } );
2188
2189         if ( C4::Context->preference('StoreLastBorrower') ) {
2190             my $item = Koha::Items->find( $itemnumber );
2191             my $patron = Koha::Patrons->find( $borrowernumber );
2192             $item->last_returned_by( $patron );
2193         }
2194     });
2195
2196     return $issue_id;
2197 }
2198
2199 =head2 _debar_user_on_return
2200
2201     _debar_user_on_return($borrower, $item, $datedue, today);
2202
2203 C<$borrower> borrower hashref
2204
2205 C<$item> item hashref
2206
2207 C<$datedue> date due DateTime object
2208
2209 C<$return_date> DateTime object representing the return time
2210
2211 Internal function, called only by AddReturn that calculates and updates
2212  the user fine days, and debars them if necessary.
2213
2214 Should only be called for overdue returns
2215
2216 =cut
2217
2218 sub _debar_user_on_return {
2219     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2220
2221     my $branchcode = _GetCircControlBranch( $item, $borrower );
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($brn,$itm, $exemptfine, $dropboxmode);
2312
2313 C<$brn> borrowernumber
2314
2315 C<$itm> itemnumber
2316
2317 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2318 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2319
2320 Internal function
2321
2322 =cut
2323
2324 sub _FixOverduesOnReturn {
2325     my ($borrowernumber, $item, $exemptfine ) = @_;
2326     unless( $borrowernumber ) {
2327         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2328         return;
2329     }
2330     unless( $item ) {
2331         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2332         return;
2333     }
2334
2335     # check for overdue fine
2336     my $accountline = Koha::Account::Lines->search(
2337         {
2338             borrowernumber => $borrowernumber,
2339             itemnumber     => $item,
2340             -or            => [
2341                 accounttype => 'FU',
2342                 accounttype => 'O',
2343             ],
2344         }
2345     )->next();
2346     return 0 unless $accountline;    # no warning, there's just nothing to fix
2347
2348     if ($exemptfine) {
2349         my $amountoutstanding = $accountline->amountoutstanding;
2350
2351         $accountline->accounttype('FFOR');
2352         $accountline->amountoutstanding(0);
2353
2354         Koha::Account::Offset->new(
2355             {
2356                 debit_id => $accountline->id,
2357                 type => 'Forgiven',
2358                 amount => $amountoutstanding * -1,
2359             }
2360         )->store();
2361
2362         if (C4::Context->preference("FinesLog")) {
2363             &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2364         }
2365     } else {
2366         $accountline->accounttype('F');
2367     }
2368
2369     return $accountline->store();
2370 }
2371
2372 =head2 _FixAccountForLostAndReturned
2373
2374   &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2375
2376 Calculates the charge for a book lost and returned.
2377
2378 Internal function, not exported, called only by AddReturn.
2379
2380 =cut
2381
2382 sub _FixAccountForLostAndReturned {
2383     my $itemnumber     = shift or return;
2384     my $borrowernumber = @_ ? shift : undef;
2385     my $item_id        = @_ ? shift : $itemnumber;  # Send the barcode if you want that logged in the description
2386
2387     my $credit;
2388
2389     # check for charge made for lost book
2390     my $accountlines = Koha::Account::Lines->search(
2391         {
2392             itemnumber  => $itemnumber,
2393             accounttype => { -in => [ 'L', 'Rep', 'W' ] },
2394         },
2395         {
2396             order_by => { -desc => [ 'date', 'accountno' ] }
2397         }
2398     );
2399
2400     return unless $accountlines->count > 0;
2401     my $accountline     = $accountlines->next;
2402     my $total_to_refund = 0;
2403     my $account = Koha::Patrons->find( $accountline->borrowernumber )->account;
2404
2405     # Use cases
2406     if ( $accountline->amount > $accountline->amountoutstanding ) {
2407         # some amount has been cancelled. collect the offsets that are not writeoffs
2408         # this works because the only way to subtract from this kind of a debt is
2409         # using the UI buttons 'Pay' and 'Write off'
2410         my $credits_offsets = Koha::Account::Offsets->search({
2411             debit_id  => $accountline->id,
2412             credit_id => { '!=' => undef }, # it is not the debit itself
2413             type      => { '!=' => 'Writeoff' },
2414             amount    => { '<'  => 0 } # credits are negative on the DB
2415         });
2416
2417         $total_to_refund = ( $credits_offsets->count > 0 )
2418                             ? $credits_offsets->total * -1 # credits are negative on the DB
2419                             : 0;
2420     }
2421
2422     my $credit_total = $accountline->amountoutstanding + $total_to_refund;
2423
2424     if ( $credit_total > 0 ) {
2425         my $branchcode = C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
2426         $credit = $account->add_credit(
2427             {   amount      => $credit_total,
2428                 description => 'Item Returned ' . $item_id,
2429                 type        => 'lost_item_return',
2430                 library_id  => $branchcode
2431             }
2432         );
2433
2434         # TODO: ->apply should just accept the accountline
2435         $credit->apply( { debits => $accountlines->reset } );
2436     }
2437
2438     # Manually set the accounttype
2439     $accountline->discard_changes->accounttype('LR');
2440     $accountline->store;
2441
2442     ModItem( { paidfor => '' }, undef, $itemnumber, { log_action => 0 } );
2443
2444     if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
2445         $account->reconcile_balance;
2446     }
2447
2448     return ($credit) ? $credit->id : undef;
2449 }
2450
2451 =head2 _GetCircControlBranch
2452
2453    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2454
2455 Internal function : 
2456
2457 Return the library code to be used to determine which circulation
2458 policy applies to a transaction.  Looks up the CircControl and
2459 HomeOrHoldingBranch system preferences.
2460
2461 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2462
2463 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2464
2465 =cut
2466
2467 sub _GetCircControlBranch {
2468     my ($item, $borrower) = @_;
2469     my $circcontrol = C4::Context->preference('CircControl');
2470     my $branch;
2471
2472     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2473         $branch= C4::Context->userenv->{'branch'};
2474     } elsif ($circcontrol eq 'PatronLibrary') {
2475         $branch=$borrower->{branchcode};
2476     } else {
2477         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2478         $branch = $item->{$branchfield};
2479         # default to item home branch if holdingbranch is used
2480         # and is not defined
2481         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2482             $branch = $item->{homebranch};
2483         }
2484     }
2485     return $branch;
2486 }
2487
2488 =head2 GetOpenIssue
2489
2490   $issue = GetOpenIssue( $itemnumber );
2491
2492 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2493
2494 C<$itemnumber> is the item's itemnumber
2495
2496 Returns a hashref
2497
2498 =cut
2499
2500 sub GetOpenIssue {
2501   my ( $itemnumber ) = @_;
2502   return unless $itemnumber;
2503   my $dbh = C4::Context->dbh;  
2504   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2505   $sth->execute( $itemnumber );
2506   return $sth->fetchrow_hashref();
2507
2508 }
2509
2510 =head2 GetBiblioIssues
2511
2512   $issues = GetBiblioIssues($biblionumber);
2513
2514 this function get all issues from a biblionumber.
2515
2516 Return:
2517 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash contains all column from
2518 tables issues and the firstname,surname & cardnumber from borrowers.
2519
2520 =cut
2521
2522 sub GetBiblioIssues {
2523     my $biblionumber = shift;
2524     return unless $biblionumber;
2525     my $dbh   = C4::Context->dbh;
2526     my $query = "
2527         SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2528         FROM issues
2529             LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2530             LEFT JOIN items ON issues.itemnumber = items.itemnumber
2531             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2532             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2533         WHERE biblio.biblionumber = ?
2534         UNION ALL
2535         SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2536         FROM old_issues
2537             LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2538             LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2539             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2540             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2541         WHERE biblio.biblionumber = ?
2542         ORDER BY timestamp
2543     ";
2544     my $sth = $dbh->prepare($query);
2545     $sth->execute($biblionumber, $biblionumber);
2546
2547     my @issues;
2548     while ( my $data = $sth->fetchrow_hashref ) {
2549         push @issues, $data;
2550     }
2551     return \@issues;
2552 }
2553
2554 =head2 GetUpcomingDueIssues
2555
2556   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2557
2558 =cut
2559
2560 sub GetUpcomingDueIssues {
2561     my $params = shift;
2562
2563     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2564     my $dbh = C4::Context->dbh;
2565
2566     my $statement = <<END_SQL;
2567 SELECT *
2568 FROM (
2569     SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2570     FROM issues
2571     LEFT JOIN items USING (itemnumber)
2572     LEFT OUTER JOIN branches USING (branchcode)
2573     WHERE returndate is NULL
2574 ) tmp
2575 WHERE days_until_due >= 0 AND days_until_due <= ?
2576 END_SQL
2577
2578     my @bind_parameters = ( $params->{'days_in_advance'} );
2579     
2580     my $sth = $dbh->prepare( $statement );
2581     $sth->execute( @bind_parameters );
2582     my $upcoming_dues = $sth->fetchall_arrayref({});
2583
2584     return $upcoming_dues;
2585 }
2586
2587 =head2 CanBookBeRenewed
2588
2589   ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2590
2591 Find out whether a borrowed item may be renewed.
2592
2593 C<$borrowernumber> is the borrower number of the patron who currently
2594 has the item on loan.
2595
2596 C<$itemnumber> is the number of the item to renew.
2597
2598 C<$override_limit>, if supplied with a true value, causes
2599 the limit on the number of times that the loan can be renewed
2600 (as controlled by the item type) to be ignored. Overriding also allows
2601 to renew sooner than "No renewal before" and to manually renew loans
2602 that are automatically renewed.
2603
2604 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2605 item must currently be on loan to the specified borrower; renewals
2606 must be allowed for the item's type; and the borrower must not have
2607 already renewed the loan. $error will contain the reason the renewal can not proceed
2608
2609 =cut
2610
2611 sub CanBookBeRenewed {
2612     my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2613
2614     my $dbh    = C4::Context->dbh;
2615     my $renews = 1;
2616
2617     my $item      = Koha::Items->find($itemnumber)      or return ( 0, 'no_item' );
2618     my $issue = $item->checkout or return ( 0, 'no_checkout' );
2619     return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2620     return ( 0, 'item_denied_renewal') if _item_denied_renewal({ item => $item });
2621
2622     my $patron = $issue->patron or return;
2623
2624     my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2625
2626     # This item can fill one or more unfilled reserve, can those unfilled reserves
2627     # all be filled by other available items?
2628     if ( $resfound
2629         && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2630     {
2631         my $schema = Koha::Database->new()->schema();
2632
2633         my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2634         if ($item_holds) {
2635             # There is an item level hold on this item, no other item can fill the hold
2636             $resfound = 1;
2637         }
2638         else {
2639
2640             # Get all other items that could possibly fill reserves
2641             my @itemnumbers = $schema->resultset('Item')->search(
2642                 {
2643                     biblionumber => $resrec->{biblionumber},
2644                     onloan       => undef,
2645                     notforloan   => 0,
2646                     -not         => { itemnumber => $itemnumber }
2647                 },
2648                 { columns => 'itemnumber' }
2649             )->get_column('itemnumber')->all();
2650
2651             # Get all other reserves that could have been filled by this item
2652             my @borrowernumbers;
2653             while (1) {
2654                 my ( $reserve_found, $reserve, undef ) =
2655                   C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2656
2657                 if ($reserve_found) {
2658                     push( @borrowernumbers, $reserve->{borrowernumber} );
2659                 }
2660                 else {
2661                     last;
2662                 }
2663             }
2664
2665             # If the count of the union of the lists of reservable items for each borrower
2666             # is equal or greater than the number of borrowers, we know that all reserves
2667             # can be filled with available items. We can get the union of the sets simply
2668             # by pushing all the elements onto an array and removing the duplicates.
2669             my @reservable;
2670             my %borrowers;
2671             ITEM: foreach my $i (@itemnumbers) {
2672                 my $item = Koha::Items->find($i)->unblessed;
2673                 next if IsItemOnHoldAndFound($i);
2674                 for my $b (@borrowernumbers) {
2675                     my $borr = $borrowers{$b} //= Koha::Patrons->find( $b )->unblessed;
2676                     next unless IsAvailableForItemLevelRequest($item, $borr);
2677                     next unless CanItemBeReserved($b,$i);
2678
2679                     push @reservable, $i;
2680                     if (@reservable >= @borrowernumbers) {
2681                         $resfound = 0;
2682                         last ITEM;
2683                     }
2684                     last;
2685                 }
2686             }
2687         }
2688     }
2689     return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2690
2691     return ( 1, undef ) if $override_limit;
2692
2693     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
2694     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2695         {   categorycode => $patron->categorycode,
2696             itemtype     => $item->effective_itemtype,
2697             branchcode   => $branchcode
2698         }
2699     );
2700
2701     return ( 0, "too_many" )
2702       if not $issuing_rule or $issuing_rule->renewalsallowed <= $issue->renewals;
2703
2704     my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2705     my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2706     $patron         = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2707     my $restricted  = $patron->is_debarred;
2708     my $hasoverdues = $patron->has_overdues;
2709
2710     if ( $restricted and $restrictionblockrenewing ) {
2711         return ( 0, 'restriction');
2712     } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2713         return ( 0, 'overdue');
2714     }
2715
2716     if ( $issue->auto_renew ) {
2717
2718         if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2719             return ( 0, 'auto_account_expired' );
2720         }
2721
2722         if ( defined $issuing_rule->no_auto_renewal_after
2723                 and $issuing_rule->no_auto_renewal_after ne "" ) {
2724             # Get issue_date and add no_auto_renewal_after
2725             # If this is greater than today, it's too late for renewal.
2726             my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2727             $maximum_renewal_date->add(
2728                 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
2729             );
2730             my $now = dt_from_string;
2731             if ( $now >= $maximum_renewal_date ) {
2732                 return ( 0, "auto_too_late" );
2733             }
2734         }
2735         if ( defined $issuing_rule->no_auto_renewal_after_hard_limit
2736                       and $issuing_rule->no_auto_renewal_after_hard_limit ne "" ) {
2737             # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2738             if ( dt_from_string >= dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit ) ) {
2739                 return ( 0, "auto_too_late" );
2740             }
2741         }
2742
2743         if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2744             my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2745             my $amountoutstanding = $patron->account->balance;
2746             if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2747                 return ( 0, "auto_too_much_oweing" );
2748             }
2749         }
2750     }
2751
2752     if ( defined $issuing_rule->norenewalbefore
2753         and $issuing_rule->norenewalbefore ne "" )
2754     {
2755
2756         # Calculate soonest renewal by subtracting 'No renewal before' from due date
2757         my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2758             $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
2759
2760         # Depending on syspref reset the exact time, only check the date
2761         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2762             and $issuing_rule->lengthunit eq 'days' )
2763         {
2764             $soonestrenewal->truncate( to => 'day' );
2765         }
2766
2767         if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
2768         {
2769             return ( 0, "auto_too_soon" ) if $issue->auto_renew;
2770             return ( 0, "too_soon" );
2771         }
2772         elsif ( $issue->auto_renew ) {
2773             return ( 0, "auto_renew" );
2774         }
2775     }
2776
2777     # Fallback for automatic renewals:
2778     # If norenewalbefore is undef, don't renew before due date.
2779     if ( $issue->auto_renew ) {
2780         my $now = dt_from_string;
2781         return ( 0, "auto_renew" )
2782           if $now >= dt_from_string( $issue->date_due, 'sql' );
2783         return ( 0, "auto_too_soon" );
2784     }
2785
2786     return ( 1, undef );
2787 }
2788
2789 =head2 AddRenewal
2790
2791   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2792
2793 Renews a loan.
2794
2795 C<$borrowernumber> is the borrower number of the patron who currently
2796 has the item.
2797
2798 C<$itemnumber> is the number of the item to renew.
2799
2800 C<$branch> is the library where the renewal took place (if any).
2801            The library that controls the circ policies for the renewal is retrieved from the issues record.
2802
2803 C<$datedue> can be a DateTime object used to set the due date.
2804
2805 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2806 this parameter is not supplied, lastreneweddate is set to the current date.
2807
2808 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2809 from the book's item type.
2810
2811 =cut
2812
2813 sub AddRenewal {
2814     my $borrowernumber  = shift;
2815     my $itemnumber      = shift or return;
2816     my $branch          = shift;
2817     my $datedue         = shift;
2818     my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz);
2819
2820     my $item_object   = Koha::Items->find($itemnumber) or return;
2821     my $biblio = $item_object->biblio;
2822     my $issue  = $item_object->checkout;
2823     my $item_unblessed = $item_object->unblessed;
2824
2825     my $dbh = C4::Context->dbh;
2826
2827     return unless $issue;
2828
2829     $borrowernumber ||= $issue->borrowernumber;
2830
2831     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2832         carp 'Invalid date passed to AddRenewal.';
2833         return;
2834     }
2835
2836     my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
2837     my $patron_unblessed = $patron->unblessed;
2838
2839     my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
2840
2841     if ( C4::Context->preference('CalculateFinesOnReturn') && $issue->is_overdue ) {
2842         _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed } );
2843     }
2844     _FixOverduesOnReturn( $borrowernumber, $itemnumber );
2845
2846     # If the due date wasn't specified, calculate it by adding the
2847     # book's loan length to today's date or the current due date
2848     # based on the value of the RenewalPeriodBase syspref.
2849     my $itemtype = $item_object->effective_itemtype;
2850     unless ($datedue) {
2851
2852         $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2853                                         dt_from_string( $issue->date_due, 'sql' ) :
2854                                         DateTime->now( time_zone => C4::Context->tz());
2855         $datedue =  CalcDateDue($datedue, $itemtype, $circ_library, $patron_unblessed, 'is a renewal');
2856     }
2857
2858     my $fees = Koha::Charges::Fees->new(
2859         {
2860             patron    => $patron,
2861             library   => $circ_library,
2862             item      => $item_object,
2863             from_date => dt_from_string( $issue->date_due, 'sql' ),
2864             to_date   => dt_from_string($datedue),
2865         }
2866     );
2867
2868     # Update the issues record to have the new due date, and a new count
2869     # of how many times it has been renewed.
2870     my $renews = $issue->renewals + 1;
2871     my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2872                             WHERE borrowernumber=? 
2873                             AND itemnumber=?"
2874     );
2875
2876     $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2877
2878     # Update the renewal count on the item, and tell zebra to reindex
2879     $renews = $item_object->renewals + 1;
2880     ModItem( { renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $item_object->biblionumber, $itemnumber, { log_action => 0 } );
2881
2882     # Charge a new rental fee, if applicable
2883     my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2884     if ( $charge > 0 ) {
2885         my $description = "Renewal of Rental Item " . $biblio->title . " " .$item_object->barcode;
2886         AddIssuingCharge($issue, $charge, $description);
2887     }
2888
2889     # Charge a new accumulate rental fee, if applicable
2890     my $itemtype_object = Koha::ItemTypes->find( $itemtype );
2891     if ( $itemtype_object ) {
2892         my $accumulate_charge = $fees->accumulate_rentalcharge();
2893         if ( $accumulate_charge > 0 ) {
2894             my $type_desc = "Renewal of Daily Rental Item " . $biblio->title . " $item_unblessed->{'barcode'}";
2895             AddIssuingCharge( $issue, $accumulate_charge, $type_desc )
2896         }
2897         $charge += $accumulate_charge;
2898     }
2899
2900     # Send a renewal slip according to checkout alert preferencei
2901     if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2902         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2903         my %conditions        = (
2904             branchcode   => $branch,
2905             categorycode => $patron->categorycode,
2906             item_type    => $itemtype,
2907             notification => 'CHECKOUT',
2908         );
2909         if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
2910             SendCirculationAlert(
2911                 {
2912                     type     => 'RENEWAL',
2913                     item     => $item_unblessed,
2914                     borrower => $patron->unblessed,
2915                     branch   => $branch,
2916                 }
2917             );
2918         }
2919     }
2920
2921     # Remove any OVERDUES related debarment if the borrower has no overdues
2922     if ( $patron
2923       && $patron->is_debarred
2924       && ! $patron->has_overdues
2925       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2926     ) {
2927         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2928     }
2929
2930     unless ( C4::Context->interface eq 'opac' ) { #if from opac we are obeying OpacRenewalBranch as calculated in opac-renew.pl
2931         $branch = C4::Context->userenv ? C4::Context->userenv->{branch} : $branch;
2932     }
2933
2934     # Add the renewal to stats
2935     UpdateStats(
2936         {
2937             branch         => $branch,
2938             type           => 'renew',
2939             amount         => $charge,
2940             itemnumber     => $itemnumber,
2941             itemtype       => $itemtype,
2942             location       => $item_object->location,
2943             borrowernumber => $borrowernumber,
2944             ccode          => $item_object->ccode,
2945         }
2946     );
2947
2948     #Log the renewal
2949     logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
2950     return $datedue;
2951 }
2952
2953 sub GetRenewCount {
2954     # check renewal status
2955     my ( $bornum, $itemno ) = @_;
2956     my $dbh           = C4::Context->dbh;
2957     my $renewcount    = 0;
2958     my $renewsallowed = 0;
2959     my $renewsleft    = 0;
2960
2961     my $patron = Koha::Patrons->find( $bornum );
2962     my $item   = Koha::Items->find($itemno);
2963
2964     return (0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
2965
2966     # Look in the issues table for this item, lent to this borrower,
2967     # and not yet returned.
2968
2969     # FIXME - I think this function could be redone to use only one SQL call.
2970     my $sth = $dbh->prepare(
2971         "select * from issues
2972                                 where (borrowernumber = ?)
2973                                 and (itemnumber = ?)"
2974     );
2975     $sth->execute( $bornum, $itemno );
2976     my $data = $sth->fetchrow_hashref;
2977     $renewcount = $data->{'renewals'} if $data->{'renewals'};
2978     # $item and $borrower should be calculated
2979     my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
2980
2981     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2982         {   categorycode => $patron->categorycode,
2983             itemtype     => $item->effective_itemtype,
2984             branchcode   => $branchcode
2985         }
2986     );
2987
2988     $renewsallowed = $issuing_rule ? $issuing_rule->renewalsallowed : 0;
2989     $renewsleft    = $renewsallowed - $renewcount;
2990     if($renewsleft < 0){ $renewsleft = 0; }
2991     return ( $renewcount, $renewsallowed, $renewsleft );
2992 }
2993
2994 =head2 GetSoonestRenewDate
2995
2996   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
2997
2998 Find out the soonest possible renew date of a borrowed item.
2999
3000 C<$borrowernumber> is the borrower number of the patron who currently
3001 has the item on loan.
3002
3003 C<$itemnumber> is the number of the item to renew.
3004
3005 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3006 renew date, based on the value "No renewal before" of the applicable
3007 issuing rule. Returns the current date if the item can already be
3008 renewed, and returns undefined if the borrower, loan, or item
3009 cannot be found.
3010
3011 =cut
3012
3013 sub GetSoonestRenewDate {
3014     my ( $borrowernumber, $itemnumber ) = @_;
3015
3016     my $dbh = C4::Context->dbh;
3017
3018     my $item      = Koha::Items->find($itemnumber)      or return;
3019     my $itemissue = $item->checkout or return;
3020
3021     $borrowernumber ||= $itemissue->borrowernumber;
3022     my $patron = Koha::Patrons->find( $borrowernumber )
3023       or return;
3024
3025     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3026     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3027         {   categorycode => $patron->categorycode,
3028             itemtype     => $item->effective_itemtype,
3029             branchcode   => $branchcode
3030         }
3031     );
3032
3033     my $now = dt_from_string;
3034     return $now unless $issuing_rule;
3035
3036     if ( defined $issuing_rule->norenewalbefore
3037         and $issuing_rule->norenewalbefore ne "" )
3038     {
3039         my $soonestrenewal =
3040           dt_from_string( $itemissue->date_due )->subtract(
3041             $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
3042
3043         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3044             and $issuing_rule->lengthunit eq 'days' )
3045         {
3046             $soonestrenewal->truncate( to => 'day' );
3047         }
3048         return $soonestrenewal if $now < $soonestrenewal;
3049     }
3050     return $now;
3051 }
3052
3053 =head2 GetLatestAutoRenewDate
3054
3055   $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3056
3057 Find out the latest possible auto renew date of a borrowed item.
3058
3059 C<$borrowernumber> is the borrower number of the patron who currently
3060 has the item on loan.
3061
3062 C<$itemnumber> is the number of the item to renew.
3063
3064 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3065 auto renew date, based on the value "No auto renewal after" and the "No auto
3066 renewal after (hard limit) of the applicable issuing rule.
3067 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3068 or item cannot be found.
3069
3070 =cut
3071
3072 sub GetLatestAutoRenewDate {
3073     my ( $borrowernumber, $itemnumber ) = @_;
3074
3075     my $dbh = C4::Context->dbh;
3076
3077     my $item      = Koha::Items->find($itemnumber)  or return;
3078     my $itemissue = $item->checkout                 or return;
3079
3080     $borrowernumber ||= $itemissue->borrowernumber;
3081     my $patron = Koha::Patrons->find( $borrowernumber )
3082       or return;
3083
3084     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3085     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3086         {   categorycode => $patron->categorycode,
3087             itemtype     => $item->effective_itemtype,
3088             branchcode   => $branchcode
3089         }
3090     );
3091
3092     return unless $issuing_rule;
3093     return
3094       if ( not $issuing_rule->no_auto_renewal_after
3095             or $issuing_rule->no_auto_renewal_after eq '' )
3096       and ( not $issuing_rule->no_auto_renewal_after_hard_limit
3097              or $issuing_rule->no_auto_renewal_after_hard_limit eq '' );
3098
3099     my $maximum_renewal_date;
3100     if ( $issuing_rule->no_auto_renewal_after ) {
3101         $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3102         $maximum_renewal_date->add(
3103             $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
3104         );
3105     }
3106
3107     if ( $issuing_rule->no_auto_renewal_after_hard_limit ) {
3108         my $dt = dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit );
3109         $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3110     }
3111     return $maximum_renewal_date;
3112 }
3113
3114
3115 =head2 GetIssuingCharges
3116
3117   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3118
3119 Calculate how much it would cost for a given patron to borrow a given
3120 item, including any applicable discounts.
3121
3122 C<$itemnumber> is the item number of item the patron wishes to borrow.
3123
3124 C<$borrowernumber> is the patron's borrower number.
3125
3126 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3127 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3128 if it's a video).
3129
3130 =cut
3131
3132 sub GetIssuingCharges {
3133
3134     # calculate charges due
3135     my ( $itemnumber, $borrowernumber ) = @_;
3136     my $charge = 0;
3137     my $dbh    = C4::Context->dbh;
3138     my $item_type;
3139
3140     # Get the book's item type and rental charge (via its biblioitem).
3141     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3142         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3143     $charge_query .= (C4::Context->preference('item-level_itypes'))
3144         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3145         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3146
3147     $charge_query .= ' WHERE items.itemnumber =?';
3148
3149     my $sth = $dbh->prepare($charge_query);
3150     $sth->execute($itemnumber);
3151     if ( my $item_data = $sth->fetchrow_hashref ) {
3152         $item_type = $item_data->{itemtype};
3153         $charge    = $item_data->{rentalcharge};
3154         my $branch = C4::Context::mybranch();
3155         my $discount_query = q|SELECT rentaldiscount,
3156             issuingrules.itemtype, issuingrules.branchcode
3157             FROM borrowers
3158             LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3159             WHERE borrowers.borrowernumber = ?
3160             AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3161             AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3162         my $discount_sth = $dbh->prepare($discount_query);
3163         $discount_sth->execute( $borrowernumber, $item_type, $branch );
3164         my $discount_rules = $discount_sth->fetchall_arrayref({});
3165         if (@{$discount_rules}) {
3166             # We may have multiple rules so get the most specific
3167             my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3168             $charge = ( $charge * ( 100 - $discount ) ) / 100;
3169         }
3170         if ($charge) {
3171             $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3172         }
3173     }
3174
3175     return ( $charge, $item_type );
3176 }
3177
3178 # Select most appropriate discount rule from those returned
3179 sub _get_discount_from_rule {
3180     my ($rules_ref, $branch, $itemtype) = @_;
3181     my $discount;
3182
3183     if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3184         $discount = $rules_ref->[0]->{rentaldiscount};
3185         return (defined $discount) ? $discount : 0;
3186     }
3187     # could have up to 4 does one match $branch and $itemtype
3188     my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3189     if (@d) {
3190         $discount = $d[0]->{rentaldiscount};
3191         return (defined $discount) ? $discount : 0;
3192     }
3193     # do we have item type + all branches
3194     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3195     if (@d) {
3196         $discount = $d[0]->{rentaldiscount};
3197         return (defined $discount) ? $discount : 0;
3198     }
3199     # do we all item types + this branch
3200     @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3201     if (@d) {
3202         $discount = $d[0]->{rentaldiscount};
3203         return (defined $discount) ? $discount : 0;
3204     }
3205     # so all and all (surely we wont get here)
3206     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3207     if (@d) {
3208         $discount = $d[0]->{rentaldiscount};
3209         return (defined $discount) ? $discount : 0;
3210     }
3211     # none of the above
3212     return 0;
3213 }
3214
3215 =head2 AddIssuingCharge
3216
3217   &AddIssuingCharge( $checkout, $charge, [$description] )
3218
3219 =cut
3220
3221 sub AddIssuingCharge {
3222     my ( $checkout, $charge, $description ) = @_;
3223
3224     # FIXME What if checkout does not exist?
3225
3226     my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3227     my $accountline = $account->add_debit(
3228         {
3229             amount      => $charge,
3230             description => $description,
3231             note        => undef,
3232             user_id     => C4::Context->userenv ? C4::Context->userenv->{'number'} : 0,
3233             library_id  => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3234             type        => 'rent',
3235             item_id     => $checkout->itemnumber,
3236             issue_id    => $checkout->issue_id,
3237         }
3238     );
3239 }
3240
3241 =head2 GetTransfers
3242
3243   GetTransfers($itemnumber);
3244
3245 =cut
3246
3247 sub GetTransfers {
3248     my ($itemnumber) = @_;
3249
3250     my $dbh = C4::Context->dbh;
3251
3252     my $query = '
3253         SELECT datesent,
3254                frombranch,
3255                tobranch,
3256                branchtransfer_id
3257         FROM branchtransfers
3258         WHERE itemnumber = ?
3259           AND datearrived IS NULL
3260         ';
3261     my $sth = $dbh->prepare($query);
3262     $sth->execute($itemnumber);
3263     my @row = $sth->fetchrow_array();
3264     return @row;
3265 }
3266
3267 =head2 GetTransfersFromTo
3268
3269   @results = GetTransfersFromTo($frombranch,$tobranch);
3270
3271 Returns the list of pending transfers between $from and $to branch
3272
3273 =cut
3274
3275 sub GetTransfersFromTo {
3276     my ( $frombranch, $tobranch ) = @_;
3277     return unless ( $frombranch && $tobranch );
3278     my $dbh   = C4::Context->dbh;
3279     my $query = "
3280         SELECT branchtransfer_id,itemnumber,datesent,frombranch
3281         FROM   branchtransfers
3282         WHERE  frombranch=?
3283           AND  tobranch=?
3284           AND datearrived IS NULL
3285     ";
3286     my $sth = $dbh->prepare($query);
3287     $sth->execute( $frombranch, $tobranch );
3288     my @gettransfers;
3289
3290     while ( my $data = $sth->fetchrow_hashref ) {
3291         push @gettransfers, $data;
3292     }
3293     return (@gettransfers);
3294 }
3295
3296 =head2 DeleteTransfer
3297
3298   &DeleteTransfer($itemnumber);
3299
3300 =cut
3301
3302 sub DeleteTransfer {
3303     my ($itemnumber) = @_;
3304     return unless $itemnumber;
3305     my $dbh          = C4::Context->dbh;
3306     my $sth          = $dbh->prepare(
3307         "DELETE FROM branchtransfers
3308          WHERE itemnumber=?
3309          AND datearrived IS NULL "
3310     );
3311     return $sth->execute($itemnumber);
3312 }
3313
3314 =head2 SendCirculationAlert
3315
3316 Send out a C<check-in> or C<checkout> alert using the messaging system.
3317
3318 B<Parameters>:
3319
3320 =over 4
3321
3322 =item type
3323
3324 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3325
3326 =item item
3327
3328 Hashref of information about the item being checked in or out.
3329
3330 =item borrower
3331
3332 Hashref of information about the borrower of the item.
3333
3334 =item branch
3335
3336 The branchcode from where the checkout or check-in took place.
3337
3338 =back
3339
3340 B<Example>:
3341
3342     SendCirculationAlert({
3343         type     => 'CHECKOUT',
3344         item     => $item,
3345         borrower => $borrower,
3346         branch   => $branch,
3347     });
3348
3349 =cut
3350
3351 sub SendCirculationAlert {
3352     my ($opts) = @_;
3353     my ($type, $item, $borrower, $branch) =
3354         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3355     my %message_name = (
3356         CHECKIN  => 'Item_Check_in',
3357         CHECKOUT => 'Item_Checkout',
3358         RENEWAL  => 'Item_Checkout',
3359     );
3360     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3361         borrowernumber => $borrower->{borrowernumber},
3362         message_name   => $message_name{$type},
3363     });
3364     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3365
3366     my $schema = Koha::Database->new->schema;
3367     my @transports = keys %{ $borrower_preferences->{transports} };
3368
3369     # From the MySQL doc:
3370     # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3371     # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3372     # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3373     my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_NO_TABLE_LOCKS};
3374
3375     for my $mtt (@transports) {
3376         my $letter =  C4::Letters::GetPreparedLetter (
3377             module => 'circulation',
3378             letter_code => $type,
3379             branchcode => $branch,
3380             message_transport_type => $mtt,
3381             lang => $borrower->{lang},
3382             tables => {
3383                 $issues_table => $item->{itemnumber},
3384                 'items'       => $item->{itemnumber},
3385                 'biblio'      => $item->{biblionumber},
3386                 'biblioitems' => $item->{biblionumber},
3387                 'borrowers'   => $borrower,
3388                 'branches'    => $branch,
3389             }
3390         ) or next;
3391
3392         $schema->storage->txn_begin;
3393         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3394         C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3395         my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3396         unless ( $message ) {
3397             C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3398             C4::Message->enqueue($letter, $borrower, $mtt);
3399         } else {
3400             $message->append($letter);
3401             $message->update;
3402         }
3403         C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3404         $schema->storage->txn_commit;
3405     }
3406
3407     return;
3408 }
3409
3410 =head2 updateWrongTransfer
3411
3412   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3413
3414 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 
3415
3416 =cut
3417
3418 sub updateWrongTransfer {
3419         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3420         my $dbh = C4::Context->dbh;     
3421 # first step validate the actual line of transfert .
3422         my $sth =
3423                 $dbh->prepare(
3424                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3425                 );
3426                 $sth->execute($FromLibrary,$itemNumber);
3427
3428 # second step create a new line of branchtransfer to the right location .
3429         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3430
3431 #third step changing holdingbranch of item
3432         UpdateHoldingbranch($FromLibrary,$itemNumber);
3433 }
3434
3435 =head2 UpdateHoldingbranch
3436
3437   $items = UpdateHoldingbranch($branch,$itmenumber);
3438
3439 Simple methode for updating hodlingbranch in items BDD line
3440
3441 =cut
3442
3443 sub UpdateHoldingbranch {
3444         my ( $branch,$itemnumber ) = @_;
3445     ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3446 }
3447
3448 =head2 CalcDateDue
3449
3450 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3451
3452 this function calculates the due date given the start date and configured circulation rules,
3453 checking against the holidays calendar as per the 'useDaysMode' syspref.
3454 C<$startdate>   = DateTime object representing start date of loan period (assumed to be today)
3455 C<$itemtype>  = itemtype code of item in question
3456 C<$branch>  = location whose calendar to use
3457 C<$borrower> = Borrower object
3458 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3459
3460 =cut
3461
3462 sub CalcDateDue {
3463     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3464
3465     $isrenewal ||= 0;
3466
3467     # loanlength now a href
3468     my $loanlength =
3469             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3470
3471     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3472             ? qq{renewalperiod}
3473             : qq{issuelength};
3474
3475     my $datedue;
3476     if ( $startdate ) {
3477         if (ref $startdate ne 'DateTime' ) {
3478             $datedue = dt_from_string($datedue);
3479         } else {
3480             $datedue = $startdate->clone;
3481         }
3482     } else {
3483         $datedue =
3484           DateTime->now( time_zone => C4::Context->tz() )
3485           ->truncate( to => 'minute' );
3486     }
3487
3488
3489     # calculate the datedue as normal
3490     if ( C4::Context->preference('useDaysMode') eq 'Days' )
3491     {    # ignoring calendar
3492         if ( $loanlength->{lengthunit} eq 'hours' ) {
3493             $datedue->add( hours => $loanlength->{$length_key} );
3494         } else {    # days
3495             $datedue->add( days => $loanlength->{$length_key} );
3496             $datedue->set_hour(23);
3497             $datedue->set_minute(59);
3498         }
3499     } else {
3500         my $dur;
3501         if ($loanlength->{lengthunit} eq 'hours') {
3502             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3503         }
3504         else { # days
3505             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3506         }
3507         my $calendar = Koha::Calendar->new( branchcode => $branch );
3508         $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3509         if ($loanlength->{lengthunit} eq 'days') {
3510             $datedue->set_hour(23);
3511             $datedue->set_minute(59);
3512         }
3513     }
3514
3515     # if Hard Due Dates are used, retrieve them and apply as necessary
3516     my ( $hardduedate, $hardduedatecompare ) =
3517       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3518     if ($hardduedate) {    # hardduedates are currently dates
3519         $hardduedate->truncate( to => 'minute' );
3520         $hardduedate->set_hour(23);
3521         $hardduedate->set_minute(59);
3522         my $cmp = DateTime->compare( $hardduedate, $datedue );
3523
3524 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3525 # if the calculated date is before the 'after' Hard Due Date (floor), override
3526 # if the hard due date is set to 'exactly', overrride
3527         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3528             $datedue = $hardduedate->clone;
3529         }
3530
3531         # in all other cases, keep the date due as it is
3532
3533     }
3534
3535     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3536     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3537         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3538         if( $expiry_dt ) { #skip empty expiry date..
3539             $expiry_dt->set( hour => 23, minute => 59);
3540             my $d1= $datedue->clone->set_time_zone('floating');
3541             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3542                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3543             }
3544         }
3545         if ( C4::Context->preference('useDaysMode') ne 'Days' ) {
3546           my $calendar = Koha::Calendar->new( branchcode => $branch );
3547           if ( $calendar->is_holiday($datedue) ) {
3548               # Don't return on a closed day
3549               $datedue = $calendar->prev_open_day( $datedue );
3550           }
3551         }
3552     }
3553
3554     return $datedue;
3555 }
3556
3557
3558 sub CheckValidBarcode{
3559 my ($barcode) = @_;
3560 my $dbh = C4::Context->dbh;
3561 my $query=qq|SELECT count(*) 
3562              FROM items 
3563              WHERE barcode=?
3564             |;
3565 my $sth = $dbh->prepare($query);
3566 $sth->execute($barcode);
3567 my $exist=$sth->fetchrow ;
3568 return $exist;
3569 }
3570
3571 =head2 IsBranchTransferAllowed
3572
3573   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3574
3575 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3576
3577 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3578 Koha::Item->can_be_transferred.
3579
3580 =cut
3581
3582 sub IsBranchTransferAllowed {
3583         my ( $toBranch, $fromBranch, $code ) = @_;
3584
3585         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3586         
3587         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3588         my $dbh = C4::Context->dbh;
3589             
3590         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3591         $sth->execute( $toBranch, $fromBranch, $code );
3592         my $limit = $sth->fetchrow_hashref();
3593                         
3594         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3595         if ( $limit->{'limitId'} ) {
3596                 return 0;
3597         } else {
3598                 return 1;
3599         }
3600 }                                                        
3601
3602 =head2 CreateBranchTransferLimit
3603
3604   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3605
3606 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3607
3608 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3609
3610 =cut
3611
3612 sub CreateBranchTransferLimit {
3613    my ( $toBranch, $fromBranch, $code ) = @_;
3614    return unless defined($toBranch) && defined($fromBranch);
3615    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3616    
3617    my $dbh = C4::Context->dbh;
3618    
3619    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3620    return $sth->execute( $code, $toBranch, $fromBranch );
3621 }
3622
3623 =head2 DeleteBranchTransferLimits
3624
3625     my $result = DeleteBranchTransferLimits($frombranch);
3626
3627 Deletes all the library transfer limits for one library.  Returns the
3628 number of limits deleted, 0e0 if no limits were deleted, or undef if
3629 no arguments are supplied.
3630
3631 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3632     fromBranch => $fromBranch
3633     })->delete.
3634
3635 =cut
3636
3637 sub DeleteBranchTransferLimits {
3638     my $branch = shift;
3639     return unless defined $branch;
3640     my $dbh    = C4::Context->dbh;
3641     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3642     return $sth->execute($branch);
3643 }
3644
3645 sub ReturnLostItem{
3646     my ( $borrowernumber, $itemnum ) = @_;
3647
3648     MarkIssueReturned( $borrowernumber, $itemnum );
3649     my $patron = Koha::Patrons->find( $borrowernumber );
3650     my $item = Koha::Items->find($itemnum);
3651     my $old_note = ($item->paidfor && ($item->paidfor ne q{})) ? $item->paidfor.' / ' : q{};
3652     my @datearr = localtime(time);
3653     my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3654     my $bor = $patron->firstname . ' ' . $patron->surname . ' ' . $patron->cardnumber;
3655     ModItem({ paidfor =>  $old_note."Paid for by $bor $date" }, undef, $itemnum);
3656 }
3657
3658
3659 sub LostItem{
3660     my ($itemnumber, $mark_lost_from, $force_mark_returned) = @_;
3661
3662     unless ( $mark_lost_from ) {
3663         # Temporary check to avoid regressions
3664         die q|LostItem called without $mark_lost_from, check the API.|;
3665     }
3666
3667     my $mark_returned;
3668     if ( $force_mark_returned ) {
3669         $mark_returned = 1;
3670     } else {
3671         my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3672         $mark_returned = ( $pref =~ m|$mark_lost_from| );
3673     }
3674
3675     my $dbh = C4::Context->dbh();
3676     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3677                            FROM issues 
3678                            JOIN items USING (itemnumber) 
3679                            JOIN biblio USING (biblionumber)
3680                            WHERE issues.itemnumber=?");
3681     $sth->execute($itemnumber);
3682     my $issues=$sth->fetchrow_hashref();
3683
3684     # If a borrower lost the item, add a replacement cost to the their record
3685     if ( my $borrowernumber = $issues->{borrowernumber} ){
3686         my $patron = Koha::Patrons->find( $borrowernumber );
3687
3688         my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 0); # 1, 0 = exemptfine, no-dropbox
3689         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3690
3691         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3692             C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'} $issues->{'itemcallnumber'}");
3693             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3694             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3695         }
3696
3697         MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned;
3698     }
3699
3700     #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch)
3701     if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) {
3702         ModItem({holdingbranch => $frombranch}, undef, $itemnumber);
3703     }
3704     my $transferdeleted = DeleteTransfer($itemnumber);
3705 }
3706
3707 sub GetOfflineOperations {
3708     my $dbh = C4::Context->dbh;
3709     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3710     $sth->execute(C4::Context->userenv->{'branch'});
3711     my $results = $sth->fetchall_arrayref({});
3712     return $results;
3713 }
3714
3715 sub GetOfflineOperation {
3716     my $operationid = shift;
3717     return unless $operationid;
3718     my $dbh = C4::Context->dbh;
3719     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3720     $sth->execute( $operationid );
3721     return $sth->fetchrow_hashref;
3722 }
3723
3724 sub AddOfflineOperation {
3725     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3726     my $dbh = C4::Context->dbh;
3727     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3728     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3729     return "Added.";
3730 }
3731
3732 sub DeleteOfflineOperation {
3733     my $dbh = C4::Context->dbh;
3734     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3735     $sth->execute( shift );
3736     return "Deleted.";
3737 }
3738
3739 sub ProcessOfflineOperation {
3740     my $operation = shift;
3741
3742     my $report;
3743     if ( $operation->{action} eq 'return' ) {
3744         $report = ProcessOfflineReturn( $operation );
3745     } elsif ( $operation->{action} eq 'issue' ) {
3746         $report = ProcessOfflineIssue( $operation );
3747     } elsif ( $operation->{action} eq 'payment' ) {
3748         $report = ProcessOfflinePayment( $operation );
3749     }
3750
3751     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3752
3753     return $report;
3754 }
3755
3756 sub ProcessOfflineReturn {
3757     my $operation = shift;
3758
3759     my $item = Koha::Items->find({barcode => $operation->{barcode}});
3760
3761     if ( $item ) {
3762         my $itemnumber = $item->itemnumber;
3763         my $issue = GetOpenIssue( $itemnumber );
3764         if ( $issue ) {
3765             MarkIssueReturned(
3766                 $issue->{borrowernumber},
3767                 $itemnumber,
3768                 $operation->{timestamp},
3769             );
3770             ModItem(
3771                 { renewals => 0, onloan => undef },
3772                 $issue->{'biblionumber'},
3773                 $itemnumber,
3774                 { log_action => 0 }
3775             );
3776             return "Success.";
3777         } else {
3778             return "Item not issued.";
3779         }
3780     } else {
3781         return "Item not found.";
3782     }
3783 }
3784
3785 sub ProcessOfflineIssue {
3786     my $operation = shift;
3787
3788     my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3789
3790     if ( $patron ) {
3791         my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3792         unless ($item) {
3793             return "Barcode not found.";
3794         }
3795         my $itemnumber = $item->itemnumber;
3796         my $issue = GetOpenIssue( $itemnumber );
3797
3798         if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3799             MarkIssueReturned(
3800                 $issue->{borrowernumber},
3801                 $itemnumber,
3802                 $operation->{timestamp},
3803             );
3804         }
3805         AddIssue(
3806             $patron->unblessed,
3807             $operation->{'barcode'},
3808             undef,
3809             1,
3810             $operation->{timestamp},
3811             undef,
3812         );
3813         return "Success.";
3814     } else {
3815         return "Borrower not found.";
3816     }
3817 }
3818
3819 sub ProcessOfflinePayment {
3820     my $operation = shift;
3821
3822     my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
3823
3824     $patron->account->pay({ amount => $operation->{amount}, library_id => $operation->{branchcode} });
3825
3826     return "Success.";
3827 }
3828
3829 =head2 TransferSlip
3830
3831   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3832
3833   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3834
3835 =cut
3836
3837 sub TransferSlip {
3838     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3839
3840     my $item =
3841       $itemnumber
3842       ? Koha::Items->find($itemnumber)
3843       : Koha::Items->find( { barcode => $barcode } );
3844
3845     $item or return;
3846
3847     return C4::Letters::GetPreparedLetter (
3848         module => 'circulation',
3849         letter_code => 'TRANSFERSLIP',
3850         branchcode => $branch,
3851         tables => {
3852             'branches'    => $to_branch,
3853             'biblio'      => $item->biblionumber,
3854             'items'       => $item->unblessed,
3855         },
3856     );
3857 }
3858
3859 =head2 CheckIfIssuedToPatron
3860
3861   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3862
3863   Return 1 if any record item is issued to patron, otherwise return 0
3864
3865 =cut
3866
3867 sub CheckIfIssuedToPatron {
3868     my ($borrowernumber, $biblionumber) = @_;
3869
3870     my $dbh = C4::Context->dbh;
3871     my $query = q|
3872         SELECT COUNT(*) FROM issues
3873         LEFT JOIN items ON items.itemnumber = issues.itemnumber
3874         WHERE items.biblionumber = ?
3875         AND issues.borrowernumber = ?
3876     |;
3877     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3878     return 1 if $is_issued;
3879     return;
3880 }
3881
3882 =head2 IsItemIssued
3883
3884   IsItemIssued( $itemnumber )
3885
3886   Return 1 if the item is on loan, otherwise return 0
3887
3888 =cut
3889
3890 sub IsItemIssued {
3891     my $itemnumber = shift;
3892     my $dbh = C4::Context->dbh;
3893     my $sth = $dbh->prepare(q{
3894         SELECT COUNT(*)
3895         FROM issues
3896         WHERE itemnumber = ?
3897     });
3898     $sth->execute($itemnumber);
3899     return $sth->fetchrow;
3900 }
3901
3902 =head2 GetAgeRestriction
3903
3904   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3905   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3906
3907   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
3908   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3909
3910 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3911 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3912 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3913          Negative days mean the borrower has gone past the age restriction age.
3914
3915 =cut
3916
3917 sub GetAgeRestriction {
3918     my ($record_restrictions, $borrower) = @_;
3919     my $markers = C4::Context->preference('AgeRestrictionMarker');
3920
3921     # Split $record_restrictions to something like FSK 16 or PEGI 6
3922     my @values = split ' ', uc($record_restrictions);
3923     return unless @values;
3924
3925     # Search first occurrence of one of the markers
3926     my @markers = split /\|/, uc($markers);
3927     return unless @markers;
3928
3929     my $index            = 0;
3930     my $restriction_year = 0;
3931     for my $value (@values) {
3932         $index++;
3933         for my $marker (@markers) {
3934             $marker =~ s/^\s+//;    #remove leading spaces
3935             $marker =~ s/\s+$//;    #remove trailing spaces
3936             if ( $marker eq $value ) {
3937                 if ( $index <= $#values ) {
3938                     $restriction_year += $values[$index];
3939                 }
3940                 last;
3941             }
3942             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3943
3944                 # Perhaps it is something like "K16" (as in Finland)
3945                 $restriction_year += $1;
3946                 last;
3947             }
3948         }
3949         last if ( $restriction_year > 0 );
3950     }
3951
3952     #Check if the borrower is age restricted for this material and for how long.
3953     if ($restriction_year && $borrower) {
3954         if ( $borrower->{'dateofbirth'} ) {
3955             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3956             $alloweddate[0] += $restriction_year;
3957
3958             #Prevent runime eror on leap year (invalid date)
3959             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3960                 $alloweddate[2] = 28;
3961             }
3962
3963             #Get how many days the borrower has to reach the age restriction
3964             my @Today = split /-/, DateTime->today->ymd();
3965             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
3966             #Negative days means the borrower went past the age restriction age
3967             return ($restriction_year, $daysToAgeRestriction);
3968         }
3969     }
3970
3971     return ($restriction_year);
3972 }
3973
3974
3975 =head2 GetPendingOnSiteCheckouts
3976
3977 =cut
3978
3979 sub GetPendingOnSiteCheckouts {
3980     my $dbh = C4::Context->dbh;
3981     return $dbh->selectall_arrayref(q|
3982         SELECT
3983           items.barcode,
3984           items.biblionumber,
3985           items.itemnumber,
3986           items.itemnotes,
3987           items.itemcallnumber,
3988           items.location,
3989           issues.date_due,
3990           issues.branchcode,
3991           issues.date_due < NOW() AS is_overdue,
3992           biblio.author,
3993           biblio.title,
3994           borrowers.firstname,
3995           borrowers.surname,
3996           borrowers.cardnumber,
3997           borrowers.borrowernumber
3998         FROM items
3999         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4000         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4001         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4002         WHERE issues.onsite_checkout = 1
4003     |, { Slice => {} } );
4004 }
4005
4006 sub GetTopIssues {
4007     my ($params) = @_;
4008
4009     my ($count, $branch, $itemtype, $ccode, $newness)
4010         = @$params{qw(count branch itemtype ccode newness)};
4011
4012     my $dbh = C4::Context->dbh;
4013     my $query = q{
4014         SELECT * FROM (
4015         SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4016           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4017           i.ccode, SUM(i.issues) AS count
4018         FROM biblio b
4019         LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4020         LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4021     };
4022
4023     my (@where_strs, @where_args);
4024
4025     if ($branch) {
4026         push @where_strs, 'i.homebranch = ?';
4027         push @where_args, $branch;
4028     }
4029     if ($itemtype) {
4030         if (C4::Context->preference('item-level_itypes')){
4031             push @where_strs, 'i.itype = ?';
4032             push @where_args, $itemtype;
4033         } else {
4034             push @where_strs, 'bi.itemtype = ?';
4035             push @where_args, $itemtype;
4036         }
4037     }
4038     if ($ccode) {
4039         push @where_strs, 'i.ccode = ?';
4040         push @where_args, $ccode;
4041     }
4042     if ($newness) {
4043         push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4044         push @where_args, $newness;
4045     }
4046
4047     if (@where_strs) {
4048         $query .= 'WHERE ' . join(' AND ', @where_strs);
4049     }
4050
4051     $query .= q{
4052         GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4053           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4054           i.ccode
4055         ORDER BY count DESC
4056     };
4057
4058     $query .= q{ ) xxx WHERE count > 0 };
4059     $count = int($count);
4060     if ($count > 0) {
4061         $query .= "LIMIT $count";
4062     }
4063
4064     my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4065
4066     return @$rows;
4067 }
4068
4069 sub _CalculateAndUpdateFine {
4070     my ($params) = @_;
4071
4072     my $borrower    = $params->{borrower};
4073     my $item        = $params->{item};
4074     my $issue       = $params->{issue};
4075     my $return_date = $params->{return_date};
4076
4077     unless ($borrower) { carp "No borrower passed in!" && return; }
4078     unless ($item)     { carp "No item passed in!"     && return; }
4079     unless ($issue)    { carp "No issue passed in!"    && return; }
4080
4081     my $datedue = dt_from_string( $issue->date_due );
4082
4083     # we only need to calculate and change the fines if we want to do that on return
4084     # Should be on for hourly loans
4085     my $control = C4::Context->preference('CircControl');
4086     my $control_branchcode =
4087         ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4088       : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
4089       :                                     $issue->branchcode;
4090
4091     my $date_returned = $return_date ? $return_date : dt_from_string();
4092
4093     my ( $amount, $unitcounttotal, $unitcount  ) =
4094       C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4095
4096     if ( C4::Context->preference('finesMode') eq 'production' ) {
4097         if ( $amount > 0 ) {
4098             C4::Overdues::UpdateFine({
4099                 issue_id       => $issue->issue_id,
4100                 itemnumber     => $issue->itemnumber,
4101                 borrowernumber => $issue->borrowernumber,
4102                 amount         => $amount,
4103                 due            => output_pref($datedue),
4104             });
4105         }
4106         elsif ($return_date) {
4107
4108             # Backdated returns may have fines that shouldn't exist,
4109             # so in this case, we need to drop those fines to 0
4110
4111             C4::Overdues::UpdateFine({
4112                 issue_id       => $issue->issue_id,
4113                 itemnumber     => $issue->itemnumber,
4114                 borrowernumber => $issue->borrowernumber,
4115                 amount         => 0,
4116                 due            => output_pref($datedue),
4117             });
4118         }
4119     }
4120 }
4121
4122 sub _item_denied_renewal {
4123     my ($params) = @_;
4124
4125     my $item = $params->{item};
4126     return unless $item;
4127
4128     my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4129     return unless $denyingrules;
4130     foreach my $field (keys %$denyingrules) {
4131         my $val = $item->$field;
4132         if( !defined $val) {
4133             if ( any { !defined $_ }  @{$denyingrules->{$field}} ){
4134                 return 1;
4135             }
4136         } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4137            # If the results matches the values in the syspref
4138            # We return true if match found
4139             return 1;
4140         }
4141     }
4142     return 0;
4143 }
4144
4145
4146 1;
4147
4148 __END__
4149
4150 =head1 AUTHOR
4151
4152 Koha Development Team <http://koha-community.org/>
4153
4154 =cut