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