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