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