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