Bug 29218: Rename .hidden class for DT visibility
[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             C4::Auth::track_login_daily( $borrower->{userid} );
1580             if ( $item_object->location && $item_object->location eq 'CART'
1581                 && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
1582             ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
1583                 CartToShelf( $item_object->itemnumber );
1584             }
1585
1586             if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1587                 UpdateTotalIssues( $item_object->biblionumber, 1 );
1588             }
1589
1590             # Record if item was lost
1591             my $was_lost = $item_object->itemlost;
1592
1593             $item_object->issues( ( $item_object->issues || 0 ) + 1);
1594             $item_object->holdingbranch(C4::Context->userenv->{'branch'});
1595             $item_object->itemlost(0);
1596             $item_object->onloan($datedue->ymd());
1597             $item_object->datelastborrowed( dt_from_string()->ymd() );
1598             $item_object->datelastseen( dt_from_string()->ymd() );
1599             $item_object->store({log_action => 0});
1600
1601             # If the item was lost, it has now been found, charge the overdue if necessary
1602             if ($was_lost) {
1603                 if ( $item_object->{_charge} ) {
1604                     $actualissue //= Koha::Old::Checkouts->search(
1605                         { itemnumber => $item_unblessed->{itemnumber} },
1606                         {
1607                             order_by => { '-desc' => 'returndate' },
1608                             rows     => 1
1609                         }
1610                     )->single;
1611                     unless ( exists( $borrower->{branchcode} ) ) {
1612                         my $patron = $actualissue->patron;
1613                         $borrower = $patron->unblessed;
1614                     }
1615                     _CalculateAndUpdateFine(
1616                         {
1617                             issue       => $actualissue,
1618                             item        => $item_unblessed,
1619                             borrower    => $borrower,
1620                             return_date => $issuedate
1621                         }
1622                     );
1623                     _FixOverduesOnReturn( $borrower->{borrowernumber},
1624                         $item_object->itemnumber, undef, 'RENEWED' );
1625                 }
1626             }
1627
1628             # If it costs to borrow this book, charge it to the patron's account.
1629             my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
1630             if ( $charge && $charge > 0 ) {
1631                 AddIssuingCharge( $issue, $charge, 'RENT' );
1632             }
1633
1634             my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1635             if ( $itemtype_object ) {
1636                 my $accumulate_charge = $fees->accumulate_rentalcharge();
1637                 if ( $accumulate_charge > 0 ) {
1638                     AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' );
1639                     $charge += $accumulate_charge;
1640                     $item_unblessed->{charge} = $charge;
1641                 }
1642             }
1643
1644             # Record the fact that this book was issued.
1645             &UpdateStats(
1646                 {
1647                     branch => C4::Context->userenv->{'branch'},
1648                     type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1649                     amount         => $charge,
1650                     other          => ( $sipmode ? "SIP-$sipmode" : '' ),
1651                     itemnumber     => $item_object->itemnumber,
1652                     itemtype       => $item_object->effective_itemtype,
1653                     location       => $item_object->location,
1654                     borrowernumber => $borrower->{'borrowernumber'},
1655                     ccode          => $item_object->ccode,
1656                 }
1657             );
1658
1659             # Send a checkout slip.
1660             my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1661             my %conditions        = (
1662                 branchcode   => $branchcode,
1663                 categorycode => $borrower->{categorycode},
1664                 item_type    => $item_object->effective_itemtype,
1665                 notification => 'CHECKOUT',
1666             );
1667             if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1668                 SendCirculationAlert(
1669                     {
1670                         type     => 'CHECKOUT',
1671                         item     => $item_object->unblessed,
1672                         borrower => $borrower,
1673                         branch   => $branchcode,
1674                     }
1675                 );
1676             }
1677             logaction(
1678                 "CIRCULATION", "ISSUE",
1679                 $borrower->{'borrowernumber'},
1680                 $item_object->itemnumber,
1681             ) if C4::Context->preference("IssueLog");
1682
1683             Koha::Plugins->call('after_circ_action', {
1684                 action  => 'checkout',
1685                 payload => {
1686                     type     => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1687                     checkout => $issue->get_from_storage
1688                 }
1689             });
1690         }
1691     }
1692     return $issue;
1693 }
1694
1695 =head2 GetLoanLength
1696
1697   my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1698
1699 Get loan length for an itemtype, a borrower type and a branch
1700
1701 =cut
1702
1703 sub GetLoanLength {
1704     my ( $categorycode, $itemtype, $branchcode ) = @_;
1705
1706     # Initialize default values
1707     my $rules = {
1708         issuelength   => 0,
1709         renewalperiod => 0,
1710         lengthunit    => 'days',
1711     };
1712
1713     my $found = Koha::CirculationRules->get_effective_rules( {
1714         branchcode => $branchcode,
1715         categorycode => $categorycode,
1716         itemtype => $itemtype,
1717         rules => [
1718             'issuelength',
1719             'renewalperiod',
1720             'lengthunit'
1721         ],
1722     } );
1723
1724     # Search for rules!
1725     foreach my $rule_name (keys %$found) {
1726         $rules->{$rule_name} = $found->{$rule_name};
1727     }
1728
1729     return $rules;
1730 }
1731
1732
1733 =head2 GetHardDueDate
1734
1735   my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1736
1737 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1738
1739 =cut
1740
1741 sub GetHardDueDate {
1742     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1743
1744     my $rules = Koha::CirculationRules->get_effective_rules(
1745         {
1746             categorycode => $borrowertype,
1747             itemtype     => $itemtype,
1748             branchcode   => $branchcode,
1749             rules        => [ 'hardduedate', 'hardduedatecompare' ],
1750         }
1751     );
1752
1753     if ( defined( $rules->{hardduedate} ) ) {
1754         if ( $rules->{hardduedate} ) {
1755             return ( dt_from_string( $rules->{hardduedate}, 'iso' ), $rules->{hardduedatecompare} );
1756         }
1757         else {
1758             return ( undef, undef );
1759         }
1760     }
1761 }
1762
1763 =head2 GetBranchBorrowerCircRule
1764
1765   my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1766
1767 Retrieves circulation rule attributes that apply to the given
1768 branch and patron category, regardless of item type.  
1769 The return value is a hashref containing the following key:
1770
1771 patron_maxissueqty - maximum number of loans that a
1772 patron of the given category can have at the given
1773 branch.  If the value is undef, no limit.
1774
1775 patron_maxonsiteissueqty - maximum of on-site checkouts that a
1776 patron of the given category can have at the given
1777 branch.  If the value is undef, no limit.
1778
1779 This will check for different branch/category combinations in the following order:
1780 branch and category
1781 branch only
1782 category only
1783 default branch and category
1784
1785 If no rule has been found in the database, it will default to
1786 the buillt in rule:
1787
1788 patron_maxissueqty - undef
1789 patron_maxonsiteissueqty - undef
1790
1791 C<$branchcode> and C<$categorycode> should contain the
1792 literal branch code and patron category code, respectively - no
1793 wildcards.
1794
1795 =cut
1796
1797 sub GetBranchBorrowerCircRule {
1798     my ( $branchcode, $categorycode ) = @_;
1799
1800     # Initialize default values
1801     my $rules = {
1802         patron_maxissueqty       => undef,
1803         patron_maxonsiteissueqty => undef,
1804     };
1805
1806     # Search for rules!
1807     foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
1808         my $rule = Koha::CirculationRules->get_effective_rule(
1809             {
1810                 categorycode => $categorycode,
1811                 itemtype     => undef,
1812                 branchcode   => $branchcode,
1813                 rule_name    => $rule_name,
1814             }
1815         );
1816
1817         $rules->{$rule_name} = $rule->rule_value if defined $rule;
1818     }
1819
1820     return $rules;
1821 }
1822
1823 =head2 GetBranchItemRule
1824
1825   my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1826
1827 Retrieves circulation rule attributes that apply to the given
1828 branch and item type, regardless of patron category.
1829
1830 The return value is a hashref containing the following keys:
1831
1832 holdallowed => Hold policy for this branch and itemtype. Possible values:
1833   not_allowed:           No holds allowed.
1834   from_home_library:     Holds allowed only by patrons that have the same homebranch as the item.
1835   from_any_library:      Holds allowed from any patron.
1836   from_local_hold_group: Holds allowed from libraries in hold group
1837
1838 returnbranch => branch to which to return item.  Possible values:
1839   noreturn: do not return, let item remain where checked in (floating collections)
1840   homebranch: return to item's home branch
1841   holdingbranch: return to issuer branch
1842
1843 This searches branchitemrules in the following order:
1844
1845   * Same branchcode and itemtype
1846   * Same branchcode, itemtype '*'
1847   * branchcode '*', same itemtype
1848   * branchcode and itemtype '*'
1849
1850 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1851
1852 =cut
1853
1854 sub GetBranchItemRule {
1855     my ( $branchcode, $itemtype ) = @_;
1856
1857     # Search for rules!
1858     my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
1859         {
1860             branchcode => $branchcode,
1861             itemtype   => $itemtype,
1862             rule_name  => 'holdallowed',
1863         }
1864     );
1865     my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
1866         {
1867             branchcode => $branchcode,
1868             itemtype   => $itemtype,
1869             rule_name  => 'hold_fulfillment_policy',
1870         }
1871     );
1872     my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
1873         {
1874             branchcode => $branchcode,
1875             itemtype   => $itemtype,
1876             rule_name  => 'returnbranch',
1877         }
1878     );
1879
1880     # built-in default circulation rule
1881     my $rules;
1882     $rules->{holdallowed} = defined $holdallowed_rule
1883         ? $holdallowed_rule->rule_value
1884         : 'from_any_library';
1885     $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
1886         ? $hold_fulfillment_policy_rule->rule_value
1887         : 'any';
1888     $rules->{returnbranch} = defined $returnbranch_rule
1889         ? $returnbranch_rule->rule_value
1890         : 'homebranch';
1891
1892     return $rules;
1893 }
1894
1895 =head2 AddReturn
1896
1897   ($doreturn, $messages, $iteminformation, $borrower) =
1898       &AddReturn( $barcode, $branch [,$exemptfine] [,$returndate] );
1899
1900 Returns a book.
1901
1902 =over 4
1903
1904 =item C<$barcode> is the bar code of the book being returned.
1905
1906 =item C<$branch> is the code of the branch where the book is being returned.
1907
1908 =item C<$exemptfine> indicates that overdue charges for the item will be
1909 removed. Optional.
1910
1911 =item C<$return_date> allows the default return date to be overridden
1912 by the given return date. Optional.
1913
1914 =back
1915
1916 C<&AddReturn> returns a list of four items:
1917
1918 C<$doreturn> is true iff the return succeeded.
1919
1920 C<$messages> is a reference-to-hash giving feedback on the operation.
1921 The keys of the hash are:
1922
1923 =over 4
1924
1925 =item C<BadBarcode>
1926
1927 No item with this barcode exists. The value is C<$barcode>.
1928
1929 =item C<NotIssued>
1930
1931 The book is not currently on loan. The value is C<$barcode>.
1932
1933 =item C<withdrawn>
1934
1935 This book has been withdrawn/cancelled. The value should be ignored.
1936
1937 =item C<Wrongbranch>
1938
1939 This book has was returned to the wrong branch.  The value is a hashref
1940 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1941 contain the branchcode of the incorrect and correct return library, respectively.
1942
1943 =item C<ResFound>
1944
1945 The item was reserved. The value is a reference-to-hash whose keys are
1946 fields from the reserves table of the Koha database, and
1947 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1948 either C<Waiting>, C<Reserved>, or 0.
1949
1950 =item C<WasReturned>
1951
1952 Value 1 if return is successful.
1953
1954 =item C<NeedsTransfer>
1955
1956 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1957
1958 =back
1959
1960 C<$iteminformation> is a reference-to-hash, giving information about the
1961 returned item from the issues table.
1962
1963 C<$borrower> is a reference-to-hash, giving information about the
1964 patron who last borrowed the book.
1965
1966 =cut
1967
1968 sub AddReturn {
1969     my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
1970
1971     if ($branch and not Koha::Libraries->find($branch)) {
1972         warn "AddReturn error: branch '$branch' not found.  Reverting to " . C4::Context->userenv->{'branch'};
1973         undef $branch;
1974     }
1975     $branch = C4::Context->userenv->{'branch'} unless $branch;  # we trust userenv to be a safe fallback/default
1976     my $return_date_specified = !!$return_date;
1977     $return_date //= dt_from_string();
1978     my $messages;
1979     my $patron;
1980     my $doreturn       = 1;
1981     my $validTransfer = 1;
1982     my $stat_type = 'return';
1983
1984     # get information on item
1985     my $item = Koha::Items->find({ barcode => $barcode });
1986     unless ($item) {
1987         return ( 0, { BadBarcode => $barcode } );    # no barcode means no item or borrower.  bail out.
1988     }
1989
1990     my $itemnumber = $item->itemnumber;
1991     my $itemtype = $item->effective_itemtype;
1992
1993     my $issue  = $item->checkout;
1994     if ( $issue ) {
1995         $patron = $issue->patron
1996             or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1997                 . Dumper($issue->unblessed) . "\n";
1998     } else {
1999         $messages->{'NotIssued'} = $barcode;
2000         $item->onloan(undef)->store({skip_record_index=>1}) if defined $item->onloan;
2001
2002         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
2003         $doreturn = 0;
2004         # No issue, no borrowernumber.  ONLY if $doreturn, *might* you have a $borrower later.
2005         # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
2006         if (C4::Context->preference("RecordLocalUseOnReturn")) {
2007            $messages->{'LocalUse'} = 1;
2008            $stat_type = 'localuse';
2009         }
2010     }
2011
2012         # full item data, but no borrowernumber or checkout info (no issue)
2013     my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
2014         # get the proper branch to which to return the item
2015     my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
2016         # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
2017     my $transfer_trigger = $hbr eq 'homebranch' ? 'ReturnToHome' : $hbr eq 'holdingbranch' ? 'ReturnToHolding' : undef;
2018
2019     my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
2020     my $patron_unblessed = $patron ? $patron->unblessed : {};
2021
2022     my $update_loc_rules = Koha::Config::SysPrefs->find('UpdateItemLocationOnCheckin')->get_yaml_pref_hash();
2023     map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
2024     if ($update_loc_rules) {
2025         if (defined $update_loc_rules->{_ALL_}) {
2026             if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
2027             if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
2028             if (
2029                 ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) ||
2030                 (!defined $item->location && $update_loc_rules->{_ALL_} ne "")
2031                ) {
2032                 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
2033                 $item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1});
2034             }
2035         }
2036         else {
2037             foreach my $key ( keys %$update_loc_rules ) {
2038                 if ( $update_loc_rules->{$key} eq '_PERM_' ) { $update_loc_rules->{$key} = $item->permanent_location; }
2039                 if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
2040                 if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
2041                     $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
2042                     $item->location($update_loc_rules->{$key})->store({skip_record_index=>1});
2043                     last;
2044                 }
2045             }
2046         }
2047     }
2048
2049     my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
2050     if ($yaml) {
2051         $yaml = "$yaml\n\n";  # YAML is anal on ending \n. Surplus does not hurt
2052         my $rules;
2053         eval { $rules = YAML::XS::Load(Encode::encode_utf8($yaml)); };
2054         if ($@) {
2055             warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
2056         }
2057         else {
2058             foreach my $key ( keys %$rules ) {
2059                 if ( $item->notforloan eq $key ) {
2060                     $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
2061                     $item->notforloan($rules->{$key})->store({ log_action => 0, skip_record_index => 1 });
2062                     last;
2063                 }
2064             }
2065         }
2066     }
2067
2068     # check if the return is allowed at this branch
2069     my ($returnallowed, $message) = CanBookBeReturned($item->unblessed, $branch);
2070     unless ($returnallowed){
2071         $messages->{'Wrongbranch'} = {
2072             Wrongbranch => $branch,
2073             Rightbranch => $message
2074         };
2075         $doreturn = 0;
2076         my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2077         $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2078         return ( $doreturn, $messages, $issue, $patron_unblessed);
2079     }
2080
2081     if ( $item->withdrawn ) { # book has been cancelled
2082         $messages->{'withdrawn'} = 1;
2083         $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
2084     }
2085
2086     if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
2087         $doreturn = 0;
2088     }
2089
2090     # case of a return of document (deal with issues and holdingbranch)
2091     if ($doreturn) {
2092         die "The item is not issed and cannot be returned" unless $issue; # Just in case...
2093         $patron or warn "AddReturn without current borrower";
2094
2095         if ($patron) {
2096             eval {
2097                 MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy, { skip_record_index => 1} );
2098             };
2099             unless ( $@ ) {
2100                 if (
2101                     (
2102                         C4::Context->preference('CalculateFinesOnReturn')
2103                         || ( $return_date_specified && C4::Context->preference('CalculateFinesOnBackdate') )
2104                     )
2105                     && !$item->itemlost
2106                   )
2107                 {
2108                     _CalculateAndUpdateFine( { issue => $issue, item => $item->unblessed, borrower => $patron_unblessed, return_date => $return_date } );
2109                 }
2110             } else {
2111                 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 );
2112
2113                 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2114                 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2115
2116                 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
2117             }
2118
2119             # FIXME is the "= 1" right?  This could be the borrower hash.
2120             $messages->{'WasReturned'} = 1;
2121
2122         } else {
2123             $item->onloan(undef)->store({ log_action => 0 , skip_record_index => 1 });
2124         }
2125     }
2126
2127     # the holdingbranch is updated if the document is returned to another location.
2128     # this is always done regardless of whether the item was on loan or not
2129     if ($item->holdingbranch ne $branch) {
2130         $item->holdingbranch($branch)->store({ skip_record_index => 1 });
2131     }
2132
2133     my $item_was_lost = $item->itemlost;
2134     my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
2135     my $updated_item = ModDateLastSeen( $item->itemnumber, $leave_item_lost, { skip_record_index => 1 } ); # will unset itemlost if needed
2136
2137     # fix up the accounts.....
2138     if ($item_was_lost) {
2139         $messages->{'WasLost'} = 1;
2140         unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
2141             $messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded};
2142             $messages->{'LostItemFeeRestored'} = $updated_item->{_restored};
2143
2144             if ( $updated_item->{_charge} ) {
2145                 $issue //= Koha::Old::Checkouts->search(
2146                     { itemnumber => $item->itemnumber },
2147                     { order_by   => { '-desc' => 'returndate' }, rows => 1 } )
2148                   ->single;
2149                 unless ( exists( $patron_unblessed->{branchcode} ) ) {
2150                     my $patron = $issue->patron;
2151                     $patron_unblessed = $patron->unblessed;
2152                 }
2153                 _CalculateAndUpdateFine(
2154                     {
2155                         issue       => $issue,
2156                         item        => $item->unblessed,
2157                         borrower    => $patron_unblessed,
2158                         return_date => $return_date
2159                     }
2160                 );
2161                 _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
2162                     $item->itemnumber, undef, 'RETURNED' );
2163                 $messages->{'LostItemFeeCharged'} = 1;
2164             }
2165         }
2166     }
2167
2168     # check if we have a transfer for this document
2169     my $transfer = $item->get_transfer;
2170
2171     # if we have a transfer to complete, we update the line of transfers with the datearrived
2172     if ($transfer) {
2173         $validTransfer = 0;
2174         if ( $transfer->in_transit ) {
2175             if ( $transfer->tobranch eq $branch ) {
2176                 $transfer->receive;
2177                 $messages->{'TransferArrived'} = $transfer->frombranch;
2178                 # validTransfer=1 allows us returning the item back if the reserve is cancelled
2179                 $validTransfer = 1 if $transfer->reason eq 'Reserve';
2180             }
2181             else {
2182                 $messages->{'WrongTransfer'}     = $transfer->tobranch;
2183                 $messages->{'WrongTransferItem'} = $item->itemnumber;
2184                 $messages->{'TransferTrigger'}   = $transfer->reason;
2185             }
2186         }
2187         else {
2188             if ( $transfer->tobranch eq $branch ) {
2189                 $transfer->receive;
2190                 $messages->{'TransferArrived'} = $transfer->frombranch;
2191                 # validTransfer=1 allows us returning the item back if the reserve is cancelled
2192                 $validTransfer = 1 if $transfer->reason eq 'Reserve';
2193             }
2194             else {
2195                 $messages->{'WasTransfered'}   = $transfer->tobranch;
2196                 $messages->{'TransferTrigger'} = $transfer->reason;
2197             }
2198         }
2199     }
2200
2201     # fix up the overdues in accounts...
2202     if ($borrowernumber) {
2203         my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
2204         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, ".$item->itemnumber."...) failed!";  # zero is OK, check defined
2205
2206         if ( $issue and $issue->is_overdue($return_date) ) {
2207         # fix fine days
2208             my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item->unblessed, dt_from_string($issue->date_due), $return_date );
2209             if ($reminder){
2210                 $messages->{'PrevDebarred'} = $debardate;
2211             } else {
2212                 $messages->{'Debarred'} = $debardate if $debardate;
2213             }
2214         # there's no overdue on the item but borrower had been previously debarred
2215         } elsif ( $issue->date_due and $patron->debarred ) {
2216              if ( $patron->debarred eq "9999-12-31") {
2217                 $messages->{'ForeverDebarred'} = $patron->debarred;
2218              } else {
2219                   my $borrower_debar_dt = dt_from_string( $patron->debarred );
2220                   $borrower_debar_dt->truncate(to => 'day');
2221                   my $today_dt = $return_date->clone()->truncate(to => 'day');
2222                   if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2223                       $messages->{'PrevDebarred'} = $patron->debarred;
2224                   }
2225              }
2226         }
2227     }
2228
2229     # find reserves.....
2230     # launch the Checkreserves routine to find any holds
2231     my ($resfound, $resrec);
2232     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2233     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2234     # 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)
2235     if ( $resfound and $resfound eq "Waiting" and $branch ne $resrec->{branchcode} ) {
2236         my $hold = C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
2237         $resfound = 'Reserved';
2238         $resrec = $hold->unblessed;
2239     }
2240     if ($resfound) {
2241           $resrec->{'ResFound'} = $resfound;
2242         $messages->{'ResFound'} = $resrec;
2243     }
2244
2245     # Record the fact that this book was returned.
2246     UpdateStats({
2247         branch         => $branch,
2248         type           => $stat_type,
2249         itemnumber     => $itemnumber,
2250         itemtype       => $itemtype,
2251         location       => $item->location,
2252         borrowernumber => $borrowernumber,
2253         ccode          => $item->ccode,
2254     });
2255
2256     # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2257     if ( $patron ) {
2258         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2259         my %conditions = (
2260             branchcode   => $branch,
2261             categorycode => $patron->categorycode,
2262             item_type    => $itemtype,
2263             notification => 'CHECKIN',
2264         );
2265         if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2266             SendCirculationAlert({
2267                 type     => 'CHECKIN',
2268                 item     => $item->unblessed,
2269                 borrower => $patron->unblessed,
2270                 branch   => $branch,
2271                 issue    => $issue
2272             });
2273         }
2274
2275         logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2276             if C4::Context->preference("ReturnLog");
2277         }
2278
2279     # Check if this item belongs to a biblio record that is attached to an
2280     # ILL request, if it is we need to update the ILL request's status
2281     if ( $doreturn and C4::Context->preference('CirculateILL')) {
2282         my $request = Koha::Illrequests->find(
2283             { biblio_id => $item->biblio->biblionumber }
2284         );
2285         $request->status('RET') if $request;
2286     }
2287
2288     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2289     if ( $validTransfer && !C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber )
2290         && ( $doreturn or $messages->{'NotIssued'} )
2291         and !$resfound
2292         and ( $branch ne $returnbranch )
2293         and not $messages->{'WrongTransfer'}
2294         and not $messages->{'WasTransfered'} )
2295     {
2296         my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2297         if  (C4::Context->preference("AutomaticItemReturn"    ) or
2298             (C4::Context->preference("UseBranchTransferLimits") and
2299              ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2300            )) {
2301             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
2302             $debug and warn "item: " . Dumper($item->unblessed);
2303             ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger, { skip_record_index => 1 });
2304             $messages->{'WasTransfered'} = $returnbranch;
2305             $messages->{'TransferTrigger'} = $transfer_trigger;
2306         } else {
2307             $messages->{'NeedsTransfer'} = $returnbranch;
2308             $messages->{'TransferTrigger'} = $transfer_trigger;
2309         }
2310     }
2311
2312     if ( C4::Context->preference('ClaimReturnedLostValue') ) {
2313         my $claims = Koha::Checkouts::ReturnClaims->search(
2314            {
2315                itemnumber => $item->id,
2316                resolution => undef,
2317            }
2318         );
2319
2320         if ( $claims->count ) {
2321             $messages->{ReturnClaims} = $claims;
2322         }
2323     }
2324
2325     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2326     $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2327
2328     if ( $doreturn and $issue ) {
2329         my $checkin = Koha::Old::Checkouts->find($issue->id);
2330
2331         Koha::Plugins->call('after_circ_action', {
2332             action  => 'checkin',
2333             payload => {
2334                 checkout=> $checkin
2335             }
2336         });
2337     }
2338
2339     return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2340 }
2341
2342 =head2 MarkIssueReturned
2343
2344   MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy, [$params] );
2345
2346 Unconditionally marks an issue as being returned by
2347 moving the C<issues> row to C<old_issues> and
2348 setting C<returndate> to the current date.
2349
2350 if C<$returndate> is specified (in iso format), it is used as the date
2351 of the return.
2352
2353 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2354 the old_issue is immediately anonymised
2355
2356 Ideally, this function would be internal to C<C4::Circulation>,
2357 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2358 and offline_circ/process_koc.pl.
2359
2360 The last optional parameter allos passing skip_record_index to the item store call.
2361
2362 =cut
2363
2364 sub MarkIssueReturned {
2365     my ( $borrowernumber, $itemnumber, $returndate, $privacy, $params ) = @_;
2366
2367     # Retrieve the issue
2368     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2369
2370     return unless $issue->borrowernumber == $borrowernumber; # If the item is checked out to another patron we do not return it
2371
2372     my $issue_id = $issue->issue_id;
2373
2374     my $anonymouspatron;
2375     if ( $privacy && $privacy == 2 ) {
2376         # The default of 0 will not work due to foreign key constraints
2377         # The anonymisation will fail if AnonymousPatron is not a valid entry
2378         # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2379         # Note that a warning should appear on the about page (System information tab).
2380         $anonymouspatron = C4::Context->preference('AnonymousPatron');
2381         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."
2382             unless Koha::Patrons->find( $anonymouspatron );
2383     }
2384
2385     my $schema = Koha::Database->schema;
2386
2387     # FIXME Improve the return value and handle it from callers
2388     $schema->txn_do(sub {
2389
2390         my $patron = Koha::Patrons->find( $borrowernumber );
2391
2392         # Update the returndate value
2393         if ( $returndate ) {
2394             $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2395         }
2396         else {
2397             $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2398         }
2399
2400         # Create the old_issues entry
2401         my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2402
2403         # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2404         if ( $privacy && $privacy == 2) {
2405             $old_checkout->borrowernumber($anonymouspatron)->store;
2406         }
2407
2408         # And finally delete the issue
2409         $issue->delete;
2410
2411         $issue->item->onloan(undef)->store({ log_action => 0, skip_record_index => $params->{skip_record_index} });
2412
2413         if ( C4::Context->preference('StoreLastBorrower') ) {
2414             my $item = Koha::Items->find( $itemnumber );
2415             $item->last_returned_by( $patron );
2416         }
2417
2418         # Remove any OVERDUES related debarment if the borrower has no overdues
2419         if ( C4::Context->preference('AutoRemoveOverduesRestrictions')
2420           && $patron->debarred
2421           && !$patron->has_overdues
2422           && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2423         ) {
2424             DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2425         }
2426
2427     });
2428
2429     return $issue_id;
2430 }
2431
2432 =head2 _debar_user_on_return
2433
2434     _debar_user_on_return($borrower, $item, $datedue, $returndate);
2435
2436 C<$borrower> borrower hashref
2437
2438 C<$item> item hashref
2439
2440 C<$datedue> date due DateTime object
2441
2442 C<$returndate> DateTime object representing the return time
2443
2444 Internal function, called only by AddReturn that calculates and updates
2445  the user fine days, and debars them if necessary.
2446
2447 Should only be called for overdue returns
2448
2449 Calculation of the debarment date has been moved to a separate subroutine _calculate_new_debar_dt
2450 to ease testing.
2451
2452 =cut
2453
2454 sub _calculate_new_debar_dt {
2455     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2456
2457     my $branchcode = _GetCircControlBranch( $item, $borrower );
2458     my $circcontrol = C4::Context->preference('CircControl');
2459     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2460         {   categorycode => $borrower->{categorycode},
2461             itemtype     => $item->{itype},
2462             branchcode   => $branchcode,
2463             rules => [
2464                 'finedays',
2465                 'lengthunit',
2466                 'firstremind',
2467                 'maxsuspensiondays',
2468                 'suspension_chargeperiod',
2469             ]
2470         }
2471     );
2472     my $finedays = $issuing_rule ? $issuing_rule->{finedays} : undef;
2473     my $unit     = $issuing_rule ? $issuing_rule->{lengthunit} : undef;
2474     my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2475
2476     return unless $finedays;
2477
2478     # finedays is in days, so hourly loans must multiply by 24
2479     # thus 1 hour late equals 1 day suspension * finedays rate
2480     $finedays = $finedays * 24 if ( $unit eq 'hours' );
2481
2482     # grace period is measured in the same units as the loan
2483     my $grace =
2484       DateTime::Duration->new( $unit => $issuing_rule->{firstremind} // 0);
2485
2486     my $deltadays = DateTime::Duration->new(
2487         days => $chargeable_units
2488     );
2489
2490     if ( $deltadays->subtract($grace)->is_positive() ) {
2491         my $suspension_days = $deltadays * $finedays;
2492
2493         if ( defined $issuing_rule->{suspension_chargeperiod} && $issuing_rule->{suspension_chargeperiod} > 1 ) {
2494             # No need to / 1 and do not consider / 0
2495             $suspension_days = DateTime::Duration->new(
2496                 days => floor( $suspension_days->in_units('days') / $issuing_rule->{suspension_chargeperiod} )
2497             );
2498         }
2499
2500         # If the max suspension days is < than the suspension days
2501         # the suspension days is limited to this maximum period.
2502         my $max_sd = $issuing_rule->{maxsuspensiondays};
2503         if ( defined $max_sd && $max_sd ne '' ) {
2504             $max_sd = DateTime::Duration->new( days => $max_sd );
2505             $suspension_days = $max_sd
2506               if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2507         }
2508
2509         my ( $has_been_extended );
2510         if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2511             my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2512             if ( $debarment ) {
2513                 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2514                 $has_been_extended = 1;
2515             }
2516         }
2517
2518         my $new_debar_dt;
2519         # Use the calendar or not to calculate the debarment date
2520         if ( C4::Context->preference('SuspensionsCalendar') eq 'noSuspensionsWhenClosed' ) {
2521             my $calendar = Koha::Calendar->new(
2522                 branchcode => $branchcode,
2523                 days_mode  => 'Calendar'
2524             );
2525             $new_debar_dt = $calendar->addDuration( $return_date, $suspension_days );
2526         }
2527         else {
2528             $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
2529         }
2530         return $new_debar_dt;
2531     }
2532     return;
2533 }
2534
2535 sub _debar_user_on_return {
2536     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2537
2538     $return_date //= dt_from_string();
2539
2540     my $new_debar_dt = _calculate_new_debar_dt ($borrower, $item, $dt_due, $return_date);
2541
2542     return unless $new_debar_dt;
2543
2544     Koha::Patron::Debarments::AddUniqueDebarment({
2545         borrowernumber => $borrower->{borrowernumber},
2546         expiration     => $new_debar_dt->ymd(),
2547         type           => 'SUSPENSION',
2548     });
2549     # if borrower was already debarred but does not get an extra debarment
2550     my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2551     my ($new_debarment_str, $is_a_reminder);
2552     if ( $borrower->{debarred} && $borrower->{debarred} eq $patron->is_debarred ) {
2553         $is_a_reminder = 1;
2554         $new_debarment_str = $borrower->{debarred};
2555     } else {
2556         $new_debarment_str = $new_debar_dt->ymd();
2557     }
2558     # FIXME Should return a DateTime object
2559     return $new_debarment_str, $is_a_reminder;
2560 }
2561
2562 =head2 _FixOverduesOnReturn
2563
2564    &_FixOverduesOnReturn($borrowernumber, $itemnumber, $exemptfine, $status);
2565
2566 C<$borrowernumber> borrowernumber
2567
2568 C<$itemnumber> itemnumber
2569
2570 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2571
2572 C<$status> ENUM -- reason for fix [ RETURNED, RENEWED, LOST, FORGIVEN ]
2573
2574 Internal function
2575
2576 =cut
2577
2578 sub _FixOverduesOnReturn {
2579     my ( $borrowernumber, $item, $exemptfine, $status ) = @_;
2580     unless( $borrowernumber ) {
2581         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2582         return;
2583     }
2584     unless( $item ) {
2585         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2586         return;
2587     }
2588     unless( $status ) {
2589         warn "_FixOverduesOnReturn() not supplied valid status";
2590         return;
2591     }
2592
2593     my $schema = Koha::Database->schema;
2594
2595     my $result = $schema->txn_do(
2596         sub {
2597             # check for overdue fine
2598             my $accountlines = Koha::Account::Lines->search(
2599                 {
2600                     borrowernumber  => $borrowernumber,
2601                     itemnumber      => $item,
2602                     debit_type_code => 'OVERDUE',
2603                     status          => 'UNRETURNED'
2604                 }
2605             );
2606             return 0 unless $accountlines->count; # no warning, there's just nothing to fix
2607
2608             my $accountline = $accountlines->next;
2609             my $payments = $accountline->credits;
2610
2611             my $amountoutstanding = $accountline->amountoutstanding;
2612             if ( $accountline->amount == 0 && $payments->count == 0 ) {
2613                 $accountline->delete;
2614                 return 0; # no warning, we've just removed a zero value fine (backdated return)
2615             } elsif ($exemptfine && ($amountoutstanding != 0)) {
2616                 my $account = Koha::Account->new({patron_id => $borrowernumber});
2617                 my $credit = $account->add_credit(
2618                     {
2619                         amount     => $amountoutstanding,
2620                         user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
2621                         library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
2622                         interface  => C4::Context->interface,
2623                         type       => 'FORGIVEN',
2624                         item_id    => $item
2625                     }
2626                 );
2627
2628                 $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
2629
2630                 if (C4::Context->preference("FinesLog")) {
2631                     &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2632                 }
2633             }
2634
2635             $accountline->status($status);
2636             return $accountline->store();
2637         }
2638     );
2639
2640     return $result;
2641 }
2642
2643 =head2 _GetCircControlBranch
2644
2645    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2646
2647 Internal function : 
2648
2649 Return the library code to be used to determine which circulation
2650 policy applies to a transaction.  Looks up the CircControl and
2651 HomeOrHoldingBranch system preferences.
2652
2653 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2654
2655 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2656
2657 =cut
2658
2659 sub _GetCircControlBranch {
2660     my ($item, $borrower) = @_;
2661     my $circcontrol = C4::Context->preference('CircControl');
2662     my $branch;
2663
2664     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2665         $branch= C4::Context->userenv->{'branch'};
2666     } elsif ($circcontrol eq 'PatronLibrary') {
2667         $branch=$borrower->{branchcode};
2668     } else {
2669         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2670         $branch = $item->{$branchfield};
2671         # default to item home branch if holdingbranch is used
2672         # and is not defined
2673         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2674             $branch = $item->{homebranch};
2675         }
2676     }
2677     return $branch;
2678 }
2679
2680 =head2 GetOpenIssue
2681
2682   $issue = GetOpenIssue( $itemnumber );
2683
2684 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2685
2686 C<$itemnumber> is the item's itemnumber
2687
2688 Returns a hashref
2689
2690 =cut
2691
2692 sub GetOpenIssue {
2693   my ( $itemnumber ) = @_;
2694   return unless $itemnumber;
2695   my $dbh = C4::Context->dbh;  
2696   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2697   $sth->execute( $itemnumber );
2698   return $sth->fetchrow_hashref();
2699
2700 }
2701
2702 =head2 GetUpcomingDueIssues
2703
2704   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2705
2706 =cut
2707
2708 sub GetUpcomingDueIssues {
2709     my $params = shift;
2710
2711     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2712     my $dbh = C4::Context->dbh;
2713     my $statement;
2714     $statement = q{
2715         SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2716         FROM issues
2717         LEFT JOIN items USING (itemnumber)
2718         LEFT JOIN branches ON branches.branchcode =
2719     };
2720     $statement .= $params->{'owning_library'} ? " items.homebranch " : " issues.branchcode ";
2721     $statement .= " WHERE returndate is NULL AND TO_DAYS( date_due )-TO_DAYS( NOW() ) BETWEEN 0 AND ?";
2722     my @bind_parameters = ( $params->{'days_in_advance'} );
2723     
2724     my $sth = $dbh->prepare( $statement );
2725     $sth->execute( @bind_parameters );
2726     my $upcoming_dues = $sth->fetchall_arrayref({});
2727
2728     return $upcoming_dues;
2729 }
2730
2731 =head2 CanBookBeRenewed
2732
2733   ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2734
2735 Find out whether a borrowed item may be renewed.
2736
2737 C<$borrowernumber> is the borrower number of the patron who currently
2738 has the item on loan.
2739
2740 C<$itemnumber> is the number of the item to renew.
2741
2742 C<$override_limit>, if supplied with a true value, causes
2743 the limit on the number of times that the loan can be renewed
2744 (as controlled by the item type) to be ignored. Overriding also allows
2745 to renew sooner than "No renewal before" and to manually renew loans
2746 that are automatically renewed.
2747
2748 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2749 item must currently be on loan to the specified borrower; renewals
2750 must be allowed for the item's type; and the borrower must not have
2751 already renewed the loan. $error will contain the reason the renewal can not proceed
2752
2753 =cut
2754
2755 sub CanBookBeRenewed {
2756     my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_;
2757
2758     my $dbh    = C4::Context->dbh;
2759     my $renews = 1;
2760     my $auto_renew = "no";
2761
2762     my $item      = Koha::Items->find($itemnumber)      or return ( 0, 'no_item' );
2763     my $issue = $item->checkout or return ( 0, 'no_checkout' );
2764     return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2765     return ( 0, 'item_denied_renewal') if _item_denied_renewal({ item => $item });
2766
2767     my $patron = $issue->patron or return;
2768
2769     # override_limit will override anything else except on_reserve
2770     unless ( $override_limit ){
2771         my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
2772         my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2773             {
2774                 categorycode => $patron->categorycode,
2775                 itemtype     => $item->effective_itemtype,
2776                 branchcode   => $branchcode,
2777                 rules => [
2778                     'renewalsallowed',
2779                     'no_auto_renewal_after',
2780                     'no_auto_renewal_after_hard_limit',
2781                     'lengthunit',
2782                     'norenewalbefore',
2783                     'unseen_renewals_allowed'
2784                 ]
2785             }
2786         );
2787
2788         return ( 0, "too_many" )
2789           if not $issuing_rule->{renewalsallowed} or $issuing_rule->{renewalsallowed} <= $issue->renewals;
2790
2791         return ( 0, "too_unseen" )
2792           if C4::Context->preference('UnseenRenewals') &&
2793             $issuing_rule->{unseen_renewals_allowed} &&
2794             $issuing_rule->{unseen_renewals_allowed} <= $issue->unseen_renewals;
2795
2796         my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2797         my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2798         $patron         = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2799         my $restricted  = $patron->is_debarred;
2800         my $hasoverdues = $patron->has_overdues;
2801
2802         if ( $restricted and $restrictionblockrenewing ) {
2803             return ( 0, 'restriction');
2804         } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2805             return ( 0, 'overdue');
2806         }
2807
2808         if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2809
2810             if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2811                 return ( 0, 'auto_account_expired' );
2812             }
2813
2814             if ( defined $issuing_rule->{no_auto_renewal_after}
2815                     and $issuing_rule->{no_auto_renewal_after} ne "" ) {
2816                 # Get issue_date and add no_auto_renewal_after
2817                 # If this is greater than today, it's too late for renewal.
2818                 my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2819                 $maximum_renewal_date->add(
2820                     $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after}
2821                 );
2822                 my $now = dt_from_string;
2823                 if ( $now >= $maximum_renewal_date ) {
2824                     return ( 0, "auto_too_late" );
2825                 }
2826             }
2827             if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit}
2828                           and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) {
2829                 # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2830                 if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) {
2831                     return ( 0, "auto_too_late" );
2832                 }
2833             }
2834
2835             if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2836                 my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2837                 my $amountoutstanding =
2838                   C4::Context->preference("OPACFineNoRenewalsIncludeCredit")
2839                   ? $patron->account->balance
2840                   : $patron->account->outstanding_debits->total_outstanding;
2841                 if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2842                     return ( 0, "auto_too_much_oweing" );
2843                 }
2844             }
2845         }
2846
2847         if ( defined $issuing_rule->{norenewalbefore}
2848             and $issuing_rule->{norenewalbefore} ne "" )
2849         {
2850
2851             # Calculate soonest renewal by subtracting 'No renewal before' from due date
2852             my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2853                 $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
2854
2855             # Depending on syspref reset the exact time, only check the date
2856             if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2857                 and $issuing_rule->{lengthunit} eq 'days' )
2858             {
2859                 $soonestrenewal->truncate( to => 'day' );
2860             }
2861
2862             if ( $soonestrenewal > dt_from_string() )
2863             {
2864                 $auto_renew = ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon";
2865             }
2866             elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2867                 $auto_renew = "ok";
2868             }
2869         }
2870
2871         # Fallback for automatic renewals:
2872         # If norenewalbefore is undef, don't renew before due date.
2873         if ( $issue->auto_renew && $auto_renew eq "no" && $patron->autorenew_checkouts ) {
2874             my $now = dt_from_string;
2875             if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
2876                 $auto_renew = "ok";
2877             } else {
2878                 $auto_renew = "auto_too_soon";
2879             }
2880         }
2881     }
2882
2883     my ( $resfound, $resrec, $possible_reserves ) = C4::Reserves::CheckReserves($itemnumber);
2884
2885     # If next hold is non priority, then check if any hold with priority (non_priority = 0) exists for the same biblionumber.
2886     if ( $resfound && $resrec->{non_priority} ) {
2887         $resfound = Koha::Holds->search(
2888             { biblionumber => $resrec->{biblionumber}, non_priority => 0 } )
2889           ->count > 0;
2890     }
2891
2892
2893
2894     # This item can fill one or more unfilled reserve, can those unfilled reserves
2895     # all be filled by other available items?
2896     if ( $resfound
2897         && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2898     {
2899         my $item_holds = Koha::Holds->search( { itemnumber => $itemnumber, found => undef } )->count();
2900         if ($item_holds) {
2901             # There is an item level hold on this item, no other item can fill the hold
2902             $resfound = 1;
2903         }
2904         else {
2905
2906             # Get all other items that could possibly fill reserves
2907             my $items = Koha::Items->search({
2908                 biblionumber => $resrec->{biblionumber},
2909                 onloan       => undef,
2910                 notforloan   => 0,
2911                 -not         => { itemnumber => $itemnumber }
2912             });
2913
2914             # Get all other reserves that could have been filled by this item
2915             my @borrowernumbers = map { $_->{borrowernumber} } @$possible_reserves;
2916             my $patrons = Koha::Patrons->search({
2917                 borrowernumber => { -in => \@borrowernumbers }
2918             });
2919
2920             # If the count of the union of the lists of reservable items for each borrower
2921             # is equal or greater than the number of borrowers, we know that all reserves
2922             # can be filled with available items. We can get the union of the sets simply
2923             # by pushing all the elements onto an array and removing the duplicates.
2924             my @reservable;
2925             ITEM: while ( my $item = $items->next ) {
2926                 next if IsItemOnHoldAndFound( $item->itemnumber );
2927                 while ( my $patron = $patrons->next ) {
2928                     next unless IsAvailableForItemLevelRequest($item, $patron);
2929                     next unless CanItemBeReserved($patron->borrowernumber,$item->itemnumber,undef,{ignore_hold_counts=>1})->{status} eq 'OK';
2930                     push @reservable, $item->itemnumber;
2931                     if (@reservable >= @borrowernumbers) {
2932                         $resfound = 0;
2933                         last ITEM;
2934                     }
2935                     last;
2936                 }
2937                 $patrons->reset;
2938             }
2939         }
2940     }
2941     if( $cron ) { #The cron wants to return 'too_soon' over 'on_reserve'
2942         return ( 0, $auto_renew  ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2943         return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2944     } else { # For other purposes we want 'on_reserve' before 'too_soon'
2945         return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2946         return ( 0, $auto_renew  ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2947     }
2948
2949     return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed
2950
2951     return ( 1, undef );
2952 }
2953
2954 =head2 AddRenewal
2955
2956   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate], [$seen]);
2957
2958 Renews a loan.
2959
2960 C<$borrowernumber> is the borrower number of the patron who currently
2961 has the item.
2962
2963 C<$itemnumber> is the number of the item to renew.
2964
2965 C<$branch> is the library where the renewal took place (if any).
2966            The library that controls the circ policies for the renewal is retrieved from the issues record.
2967
2968 C<$datedue> can be a DateTime object used to set the due date.
2969
2970 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2971 this parameter is not supplied, lastreneweddate is set to the current date.
2972
2973 C<$skipfinecalc> is an optional boolean. There may be circumstances where, even if the
2974 CalculateFinesOnReturn syspref is enabled, we don't want to calculate fines upon renew,
2975 for example, when we're renewing as a result of a fine being paid (see RenewAccruingItemWhenPaid
2976 syspref)
2977
2978 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2979 from the book's item type.
2980
2981 C<$seen> is a boolean flag indicating if the item was seen or not during the renewal. This
2982 informs the incrementing of the unseen_renewals column. If this flag is not supplied, we
2983 fallback to a true value
2984
2985 =cut
2986
2987 sub AddRenewal {
2988     my $borrowernumber  = shift;
2989     my $itemnumber      = shift or return;
2990     my $branch          = shift;
2991     my $datedue         = shift;
2992     my $lastreneweddate = shift || dt_from_string();
2993     my $skipfinecalc    = shift;
2994     my $seen            = shift;
2995
2996     # Fallback on a 'seen' renewal
2997     $seen = defined $seen && $seen == 0 ? 0 : 1;
2998
2999     my $item_object   = Koha::Items->find($itemnumber) or return;
3000     my $biblio = $item_object->biblio;
3001     my $issue  = $item_object->checkout;
3002     my $item_unblessed = $item_object->unblessed;
3003
3004     my $dbh = C4::Context->dbh;
3005
3006     return unless $issue;
3007
3008     $borrowernumber ||= $issue->borrowernumber;
3009
3010     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
3011         carp 'Invalid date passed to AddRenewal.';
3012         return;
3013     }
3014
3015     my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
3016     my $patron_unblessed = $patron->unblessed;
3017
3018     my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
3019
3020     my $schema = Koha::Database->schema;
3021     $schema->txn_do(sub{
3022
3023         if ( !$skipfinecalc && C4::Context->preference('CalculateFinesOnReturn') ) {
3024             _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed } );
3025         }
3026         _FixOverduesOnReturn( $borrowernumber, $itemnumber, undef, 'RENEWED' );
3027
3028         # If the due date wasn't specified, calculate it by adding the
3029         # book's loan length to today's date or the current due date
3030         # based on the value of the RenewalPeriodBase syspref.
3031         my $itemtype = $item_object->effective_itemtype;
3032         unless ($datedue) {
3033
3034             $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
3035                                             dt_from_string( $issue->date_due, 'sql' ) :
3036                                             dt_from_string();
3037             $datedue =  CalcDateDue($datedue, $itemtype, $circ_library->branchcode, $patron_unblessed, 'is a renewal');
3038         }
3039
3040         my $fees = Koha::Charges::Fees->new(
3041             {
3042                 patron    => $patron,
3043                 library   => $circ_library,
3044                 item      => $item_object,
3045                 from_date => dt_from_string( $issue->date_due, 'sql' ),
3046                 to_date   => dt_from_string($datedue),
3047             }
3048         );
3049
3050         # Increment the unseen renewals, if appropriate
3051         # We only do so if the syspref is enabled and
3052         # a maximum value has been set in the circ rules
3053         my $unseen_renewals = $issue->unseen_renewals;
3054         if (C4::Context->preference('UnseenRenewals')) {
3055             my $rule = Koha::CirculationRules->get_effective_rule(
3056                 {   categorycode => $patron->categorycode,
3057                     itemtype     => $item_object->effective_itemtype,
3058                     branchcode   => $circ_library->branchcode,
3059                     rule_name    => 'unseen_renewals_allowed'
3060                 }
3061             );
3062             if (!$seen && $rule && $rule->rule_value) {
3063                 $unseen_renewals++;
3064             } else {
3065                 # If the renewal is seen, unseen should revert to 0
3066                 $unseen_renewals = 0;
3067             }
3068         }
3069
3070         # Update the issues record to have the new due date, and a new count
3071         # of how many times it has been renewed.
3072         my $renews = ( $issue->renewals || 0 ) + 1;
3073         my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, unseen_renewals = ?, lastreneweddate = ? WHERE issue_id = ?");
3074
3075         eval{
3076             $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $issue->issue_id );
3077         };
3078         if( $sth->err ){
3079             Koha::Exceptions::Checkout::FailedRenewal->throw(
3080                 error => 'Update of issue# ' . $issue->issue_id . ' failed with error: ' . $sth->errstr
3081             );
3082         }
3083
3084         # Update the renewal count on the item, and tell zebra to reindex
3085         $renews = ( $item_object->renewals || 0 ) + 1;
3086         $item_object->renewals($renews);
3087         $item_object->onloan($datedue);
3088         $item_object->store({ log_action => 0 });
3089
3090         # Charge a new rental fee, if applicable
3091         my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
3092         if ( $charge > 0 ) {
3093             AddIssuingCharge($issue, $charge, 'RENT_RENEW');
3094         }
3095
3096         # Charge a new accumulate rental fee, if applicable
3097         my $itemtype_object = Koha::ItemTypes->find( $itemtype );
3098         if ( $itemtype_object ) {
3099             my $accumulate_charge = $fees->accumulate_rentalcharge();
3100             if ( $accumulate_charge > 0 ) {
3101                 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY_RENEW' )
3102             }
3103             $charge += $accumulate_charge;
3104         }
3105
3106         # Send a renewal slip according to checkout alert preferencei
3107         if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
3108             my $circulation_alert = 'C4::ItemCirculationAlertPreference';
3109             my %conditions        = (
3110                 branchcode   => $branch,
3111                 categorycode => $patron->categorycode,
3112                 item_type    => $itemtype,
3113                 notification => 'CHECKOUT',
3114             );
3115             if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
3116                 SendCirculationAlert(
3117                     {
3118                         type     => 'RENEWAL',
3119                         item     => $item_unblessed,
3120                         borrower => $patron->unblessed,
3121                         branch   => $branch,
3122                     }
3123                 );
3124             }
3125         }
3126
3127         # Remove any OVERDUES related debarment if the borrower has no overdues
3128         if ( $patron
3129           && $patron->is_debarred
3130           && ! $patron->has_overdues
3131           && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
3132         ) {
3133             DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
3134         }
3135
3136         # Add the renewal to stats
3137         UpdateStats(
3138             {
3139                 branch         => $item_object->renewal_branchcode({branch => $branch}),
3140                 type           => 'renew',
3141                 amount         => $charge,
3142                 itemnumber     => $itemnumber,
3143                 itemtype       => $itemtype,
3144                 location       => $item_object->location,
3145                 borrowernumber => $borrowernumber,
3146                 ccode          => $item_object->ccode,
3147             }
3148         );
3149
3150         #Log the renewal
3151         logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
3152
3153         Koha::Plugins->call('after_circ_action', {
3154             action  => 'renewal',
3155             payload => {
3156                 checkout  => $issue->get_from_storage
3157             }
3158         });
3159     });
3160
3161     return $datedue;
3162 }
3163
3164 sub GetRenewCount {
3165     # check renewal status
3166     my ( $bornum, $itemno ) = @_;
3167     my $dbh           = C4::Context->dbh;
3168     my $renewcount    = 0;
3169     my $unseencount    = 0;
3170     my $renewsallowed = 0;
3171     my $unseenallowed = 0;
3172     my $renewsleft    = 0;
3173     my $unseenleft    = 0;
3174
3175     my $patron = Koha::Patrons->find( $bornum );
3176     my $item   = Koha::Items->find($itemno);
3177
3178     return (0, 0, 0, 0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
3179
3180     # Look in the issues table for this item, lent to this borrower,
3181     # and not yet returned.
3182
3183     # FIXME - I think this function could be redone to use only one SQL call.
3184     my $sth = $dbh->prepare(
3185         "select * from issues
3186                                 where (borrowernumber = ?)
3187                                 and (itemnumber = ?)"
3188     );
3189     $sth->execute( $bornum, $itemno );
3190     my $data = $sth->fetchrow_hashref;
3191     $renewcount = $data->{'renewals'} if $data->{'renewals'};
3192     $unseencount = $data->{'unseen_renewals'} if $data->{'unseen_renewals'};
3193     # $item and $borrower should be calculated
3194     my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
3195
3196     my $rules = Koha::CirculationRules->get_effective_rules(
3197         {
3198             categorycode => $patron->categorycode,
3199             itemtype     => $item->effective_itemtype,
3200             branchcode   => $branchcode,
3201             rules        => [ 'renewalsallowed', 'unseen_renewals_allowed' ]
3202         }
3203     );
3204     $renewsallowed = $rules ? $rules->{renewalsallowed} : 0;
3205     $unseenallowed = $rules->{unseen_renewals_allowed} ?
3206         $rules->{unseen_renewals_allowed} :
3207         0;
3208     $renewsleft    = $renewsallowed - $renewcount;
3209     $unseenleft    = $unseenallowed - $unseencount;
3210     if($renewsleft < 0){ $renewsleft = 0; }
3211     if($unseenleft < 0){ $unseenleft = 0; }
3212     return (
3213         $renewcount,
3214         $renewsallowed,
3215         $renewsleft,
3216         $unseencount,
3217         $unseenallowed,
3218         $unseenleft
3219     );
3220 }
3221
3222 =head2 GetSoonestRenewDate
3223
3224   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3225
3226 Find out the soonest possible renew date of a borrowed item.
3227
3228 C<$borrowernumber> is the borrower number of the patron who currently
3229 has the item on loan.
3230
3231 C<$itemnumber> is the number of the item to renew.
3232
3233 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3234 renew date, based on the value "No renewal before" of the applicable
3235 issuing rule. Returns the current date if the item can already be
3236 renewed, and returns undefined if the borrower, loan, or item
3237 cannot be found.
3238
3239 =cut
3240
3241 sub GetSoonestRenewDate {
3242     my ( $borrowernumber, $itemnumber ) = @_;
3243
3244     my $dbh = C4::Context->dbh;
3245
3246     my $item      = Koha::Items->find($itemnumber)      or return;
3247     my $itemissue = $item->checkout or return;
3248
3249     $borrowernumber ||= $itemissue->borrowernumber;
3250     my $patron = Koha::Patrons->find( $borrowernumber )
3251       or return;
3252
3253     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3254     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
3255         {   categorycode => $patron->categorycode,
3256             itemtype     => $item->effective_itemtype,
3257             branchcode   => $branchcode,
3258             rules => [
3259                 'norenewalbefore',
3260                 'lengthunit',
3261             ]
3262         }
3263     );
3264
3265     my $now = dt_from_string;
3266     return $now unless $issuing_rule;
3267
3268     if ( defined $issuing_rule->{norenewalbefore}
3269         and $issuing_rule->{norenewalbefore} ne "" )
3270     {
3271         my $soonestrenewal =
3272           dt_from_string( $itemissue->date_due )->subtract(
3273             $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
3274
3275         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3276             and $issuing_rule->{lengthunit} eq 'days' )
3277         {
3278             $soonestrenewal->truncate( to => 'day' );
3279         }
3280         return $soonestrenewal if $now < $soonestrenewal;
3281     }
3282     return $now;
3283 }
3284
3285 =head2 GetLatestAutoRenewDate
3286
3287   $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3288
3289 Find out the latest possible auto renew date of a borrowed item.
3290
3291 C<$borrowernumber> is the borrower number of the patron who currently
3292 has the item on loan.
3293
3294 C<$itemnumber> is the number of the item to renew.
3295
3296 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3297 auto renew date, based on the value "No auto renewal after" and the "No auto
3298 renewal after (hard limit) of the applicable issuing rule.
3299 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3300 or item cannot be found.
3301
3302 =cut
3303
3304 sub GetLatestAutoRenewDate {
3305     my ( $borrowernumber, $itemnumber ) = @_;
3306
3307     my $dbh = C4::Context->dbh;
3308
3309     my $item      = Koha::Items->find($itemnumber)  or return;
3310     my $itemissue = $item->checkout                 or return;
3311
3312     $borrowernumber ||= $itemissue->borrowernumber;
3313     my $patron = Koha::Patrons->find( $borrowernumber )
3314       or return;
3315
3316     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3317     my $circulation_rules = Koha::CirculationRules->get_effective_rules(
3318         {
3319             categorycode => $patron->categorycode,
3320             itemtype     => $item->effective_itemtype,
3321             branchcode   => $branchcode,
3322             rules => [
3323                 'no_auto_renewal_after',
3324                 'no_auto_renewal_after_hard_limit',
3325                 'lengthunit',
3326             ]
3327         }
3328     );
3329
3330     return unless $circulation_rules;
3331     return
3332       if ( not $circulation_rules->{no_auto_renewal_after}
3333             or $circulation_rules->{no_auto_renewal_after} eq '' )
3334       and ( not $circulation_rules->{no_auto_renewal_after_hard_limit}
3335              or $circulation_rules->{no_auto_renewal_after_hard_limit} eq '' );
3336
3337     my $maximum_renewal_date;
3338     if ( $circulation_rules->{no_auto_renewal_after} ) {
3339         $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3340         $maximum_renewal_date->add(
3341             $circulation_rules->{lengthunit} => $circulation_rules->{no_auto_renewal_after}
3342         );
3343     }
3344
3345     if ( $circulation_rules->{no_auto_renewal_after_hard_limit} ) {
3346         my $dt = dt_from_string( $circulation_rules->{no_auto_renewal_after_hard_limit} );
3347         $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3348     }
3349     return $maximum_renewal_date;
3350 }
3351
3352
3353 =head2 GetIssuingCharges
3354
3355   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3356
3357 Calculate how much it would cost for a given patron to borrow a given
3358 item, including any applicable discounts.
3359
3360 C<$itemnumber> is the item number of item the patron wishes to borrow.
3361
3362 C<$borrowernumber> is the patron's borrower number.
3363
3364 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3365 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3366 if it's a video).
3367
3368 =cut
3369
3370 sub GetIssuingCharges {
3371
3372     # calculate charges due
3373     my ( $itemnumber, $borrowernumber ) = @_;
3374     my $charge = 0;
3375     my $dbh    = C4::Context->dbh;
3376     my $item_type;
3377
3378     # Get the book's item type and rental charge (via its biblioitem).
3379     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3380         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3381     $charge_query .= (C4::Context->preference('item-level_itypes'))
3382         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3383         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3384
3385     $charge_query .= ' WHERE items.itemnumber =?';
3386
3387     my $sth = $dbh->prepare($charge_query);
3388     $sth->execute($itemnumber);
3389     if ( my $item_data = $sth->fetchrow_hashref ) {
3390         $item_type = $item_data->{itemtype};
3391         $charge    = $item_data->{rentalcharge};
3392         if ($charge) {
3393             # FIXME This should follow CircControl
3394             my $branch = C4::Context::mybranch();
3395             my $patron = Koha::Patrons->find( $borrowernumber );
3396             my $discount = Koha::CirculationRules->get_effective_rule({
3397                 categorycode => $patron->categorycode,
3398                 branchcode   => $branch,
3399                 itemtype     => $item_type,
3400                 rule_name    => 'rentaldiscount'
3401             });
3402             if ($discount) {
3403                 $charge = ( $charge * ( 100 - $discount->rule_value ) ) / 100;
3404             }
3405             $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3406         }
3407     }
3408
3409     return ( $charge, $item_type );
3410 }
3411
3412 =head2 AddIssuingCharge
3413
3414   &AddIssuingCharge( $checkout, $charge, $type )
3415
3416 =cut
3417
3418 sub AddIssuingCharge {
3419     my ( $checkout, $charge, $type ) = @_;
3420
3421     # FIXME What if checkout does not exist?
3422
3423     my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3424     my $accountline = $account->add_debit(
3425         {
3426             amount      => $charge,
3427             note        => undef,
3428             user_id     => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
3429             library_id  => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3430             interface   => C4::Context->interface,
3431             type        => $type,
3432             item_id     => $checkout->itemnumber,
3433             issue_id    => $checkout->issue_id,
3434         }
3435     );
3436 }
3437
3438 =head2 GetTransfers
3439
3440   GetTransfers($itemnumber);
3441
3442 =cut
3443
3444 sub GetTransfers {
3445     my ($itemnumber) = @_;
3446
3447     my $dbh = C4::Context->dbh;
3448
3449     my $query = '
3450         SELECT datesent,
3451                frombranch,
3452                tobranch,
3453                branchtransfer_id,
3454                daterequested,
3455                reason
3456         FROM branchtransfers
3457         WHERE itemnumber = ?
3458           AND datearrived IS NULL
3459           AND datecancelled IS NULL
3460         ';
3461     my $sth = $dbh->prepare($query);
3462     $sth->execute($itemnumber);
3463     my @row = $sth->fetchrow_array();
3464     return @row;
3465 }
3466
3467 =head2 GetTransfersFromTo
3468
3469   @results = GetTransfersFromTo($frombranch,$tobranch);
3470
3471 Returns the list of pending transfers between $from and $to branch
3472
3473 =cut
3474
3475 sub GetTransfersFromTo {
3476     my ( $frombranch, $tobranch ) = @_;
3477     return unless ( $frombranch && $tobranch );
3478     my $dbh   = C4::Context->dbh;
3479     my $query = "
3480         SELECT branchtransfer_id,itemnumber,datesent,frombranch
3481         FROM   branchtransfers
3482         WHERE  frombranch=?
3483           AND  tobranch=?
3484           AND datecancelled IS NULL
3485           AND datesent IS NOT NULL
3486           AND datearrived IS NULL
3487     ";
3488     my $sth = $dbh->prepare($query);
3489     $sth->execute( $frombranch, $tobranch );
3490     my @gettransfers;
3491
3492     while ( my $data = $sth->fetchrow_hashref ) {
3493         push @gettransfers, $data;
3494     }
3495     return (@gettransfers);
3496 }
3497
3498 =head2 SendCirculationAlert
3499
3500 Send out a C<check-in> or C<checkout> alert using the messaging system.
3501
3502 B<Parameters>:
3503
3504 =over 4
3505
3506 =item type
3507
3508 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3509
3510 =item item
3511
3512 Hashref of information about the item being checked in or out.
3513
3514 =item borrower
3515
3516 Hashref of information about the borrower of the item.
3517
3518 =item branch
3519
3520 The branchcode from where the checkout or check-in took place.
3521
3522 =back
3523
3524 B<Example>:
3525
3526     SendCirculationAlert({
3527         type     => 'CHECKOUT',
3528         item     => $item,
3529         borrower => $borrower,
3530         branch   => $branch,
3531     });
3532
3533 =cut
3534
3535 sub SendCirculationAlert {
3536     my ($opts) = @_;
3537     my ($type, $item, $borrower, $branch, $issue) =
3538         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch}, $opts->{issue});
3539     my %message_name = (
3540         CHECKIN  => 'Item_Check_in',
3541         CHECKOUT => 'Item_Checkout',
3542         RENEWAL  => 'Item_Checkout',
3543     );
3544     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3545         borrowernumber => $borrower->{borrowernumber},
3546         message_name   => $message_name{$type},
3547     });
3548
3549
3550     my $tables = {
3551         items => $item->{itemnumber},
3552         biblio      => $item->{biblionumber},
3553         biblioitems => $item->{biblionumber},
3554         borrowers   => $borrower,
3555         branches    => $branch,
3556     };
3557
3558     # TODO: Currently, we need to pass an issue_id as identifier for old_issues, but still an itemnumber for issues.
3559     # See C4::Letters:: _parseletter_sth
3560     if( $type eq 'CHECKIN' ){
3561         $tables->{old_issues} = $issue->issue_id;
3562     } else {
3563         $tables->{issues} = $item->{itemnumber};
3564     }
3565
3566     my $schema = Koha::Database->new->schema;
3567     my @transports = keys %{ $borrower_preferences->{transports} };
3568
3569     # From the MySQL doc:
3570     # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3571     # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3572     # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3573     my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_TESTING};
3574
3575     for my $mtt (@transports) {
3576         my $letter =  C4::Letters::GetPreparedLetter (
3577             module => 'circulation',
3578             letter_code => $type,
3579             branchcode => $branch,
3580             message_transport_type => $mtt,
3581             lang => $borrower->{lang},
3582             tables => $tables,
3583         ) or next;
3584
3585         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3586         C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3587         my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3588         unless ( $message ) {
3589             C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3590             C4::Message->enqueue($letter, $borrower, $mtt);
3591         } else {
3592             $message->append($letter);
3593             $message->update;
3594         }
3595         C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3596     }
3597
3598     return;
3599 }
3600
3601 =head2 updateWrongTransfer
3602
3603   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3604
3605 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 
3606
3607 =cut
3608
3609 sub updateWrongTransfer {
3610         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3611
3612     # first step: cancel the original transfer
3613     my $item = Koha::Items->find($itemNumber);
3614     my $transfer = $item->get_transfer;
3615     $transfer->set({ datecancelled => dt_from_string, cancellation_reason => 'WrongTransfer' })->store();
3616
3617     # second step: create a new transfer to the right location
3618     my $new_transfer = $item->request_transfer(
3619         {
3620             to            => $transfer->to_library,
3621             reason        => $transfer->reason,
3622             comment       => $transfer->comments,
3623             ignore_limits => 1,
3624             enqueue       => 1
3625         }
3626     );
3627
3628     return $new_transfer;
3629 }
3630
3631 =head2 CalcDateDue
3632
3633 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3634
3635 this function calculates the due date given the start date and configured circulation rules,
3636 checking against the holidays calendar as per the daysmode circulation rule.
3637 C<$startdate>   = DateTime object representing start date of loan period (assumed to be today)
3638 C<$itemtype>  = itemtype code of item in question
3639 C<$branch>  = location whose calendar to use
3640 C<$borrower> = Borrower object
3641 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3642
3643 =cut
3644
3645 sub CalcDateDue {
3646     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3647
3648     $isrenewal ||= 0;
3649
3650     # loanlength now a href
3651     my $loanlength =
3652             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3653
3654     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3655             ? qq{renewalperiod}
3656             : qq{issuelength};
3657
3658     my $datedue;
3659     if ( $startdate ) {
3660         if (ref $startdate ne 'DateTime' ) {
3661             $datedue = dt_from_string($datedue);
3662         } else {
3663             $datedue = $startdate->clone;
3664         }
3665     } else {
3666         $datedue = dt_from_string()->truncate( to => 'minute' );
3667     }
3668
3669
3670     my $daysmode = Koha::CirculationRules->get_effective_daysmode(
3671         {
3672             categorycode => $borrower->{categorycode},
3673             itemtype     => $itemtype,
3674             branchcode   => $branch,
3675         }
3676     );
3677
3678     # calculate the datedue as normal
3679     if ( $daysmode eq 'Days' )
3680     {    # ignoring calendar
3681         if ( $loanlength->{lengthunit} eq 'hours' ) {
3682             $datedue->add( hours => $loanlength->{$length_key} );
3683         } else {    # days
3684             $datedue->add( days => $loanlength->{$length_key} );
3685             $datedue->set_hour(23);
3686             $datedue->set_minute(59);
3687         }
3688     } else {
3689         my $dur;
3690         if ($loanlength->{lengthunit} eq 'hours') {
3691             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3692         }
3693         else { # days
3694             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3695         }
3696         my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3697         $datedue = $calendar->addDuration( $datedue, $dur, $loanlength->{lengthunit} );
3698         if ($loanlength->{lengthunit} eq 'days') {
3699             $datedue->set_hour(23);
3700             $datedue->set_minute(59);
3701         }
3702     }
3703
3704     # if Hard Due Dates are used, retrieve them and apply as necessary
3705     my ( $hardduedate, $hardduedatecompare ) =
3706       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3707     if ($hardduedate) {    # hardduedates are currently dates
3708         $hardduedate->truncate( to => 'minute' );
3709         $hardduedate->set_hour(23);
3710         $hardduedate->set_minute(59);
3711         my $cmp = DateTime->compare( $hardduedate, $datedue );
3712
3713 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3714 # if the calculated date is before the 'after' Hard Due Date (floor), override
3715 # if the hard due date is set to 'exactly', overrride
3716         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3717             $datedue = $hardduedate->clone;
3718         }
3719
3720         # in all other cases, keep the date due as it is
3721
3722     }
3723
3724     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3725     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3726         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3727         if( $expiry_dt ) { #skip empty expiry date..
3728             $expiry_dt->set( hour => 23, minute => 59);
3729             my $d1= $datedue->clone->set_time_zone('floating');
3730             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3731                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3732             }
3733         }
3734         if ( $daysmode ne 'Days' ) {
3735           my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3736           if ( $calendar->is_holiday($datedue) ) {
3737               # Don't return on a closed day
3738               $datedue = $calendar->prev_open_days( $datedue, 1 );
3739           }
3740         }
3741     }
3742
3743     return $datedue;
3744 }
3745
3746
3747 sub CheckValidBarcode{
3748 my ($barcode) = @_;
3749 my $dbh = C4::Context->dbh;
3750 my $query=qq|SELECT count(*) 
3751              FROM items 
3752              WHERE barcode=?
3753             |;
3754 my $sth = $dbh->prepare($query);
3755 $sth->execute($barcode);
3756 my $exist=$sth->fetchrow ;
3757 return $exist;
3758 }
3759
3760 =head2 IsBranchTransferAllowed
3761
3762   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3763
3764 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3765
3766 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3767 Koha::Item->can_be_transferred.
3768
3769 =cut
3770
3771 sub IsBranchTransferAllowed {
3772         my ( $toBranch, $fromBranch, $code ) = @_;
3773
3774         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3775         
3776         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3777         my $dbh = C4::Context->dbh;
3778             
3779         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3780         $sth->execute( $toBranch, $fromBranch, $code );
3781         my $limit = $sth->fetchrow_hashref();
3782                         
3783         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3784         if ( $limit->{'limitId'} ) {
3785                 return 0;
3786         } else {
3787                 return 1;
3788         }
3789 }                                                        
3790
3791 =head2 CreateBranchTransferLimit
3792
3793   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3794
3795 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3796
3797 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3798
3799 =cut
3800
3801 sub CreateBranchTransferLimit {
3802    my ( $toBranch, $fromBranch, $code ) = @_;
3803    return unless defined($toBranch) && defined($fromBranch);
3804    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3805    
3806    my $dbh = C4::Context->dbh;
3807    
3808    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3809    return $sth->execute( $code, $toBranch, $fromBranch );
3810 }
3811
3812 =head2 DeleteBranchTransferLimits
3813
3814     my $result = DeleteBranchTransferLimits($frombranch);
3815
3816 Deletes all the library transfer limits for one library.  Returns the
3817 number of limits deleted, 0e0 if no limits were deleted, or undef if
3818 no arguments are supplied.
3819
3820 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3821     fromBranch => $fromBranch
3822     })->delete.
3823
3824 =cut
3825
3826 sub DeleteBranchTransferLimits {
3827     my $branch = shift;
3828     return unless defined $branch;
3829     my $dbh    = C4::Context->dbh;
3830     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3831     return $sth->execute($branch);
3832 }
3833
3834 sub ReturnLostItem{
3835     my ( $borrowernumber, $itemnum ) = @_;
3836     MarkIssueReturned( $borrowernumber, $itemnum );
3837 }
3838
3839 =head2 LostItem
3840
3841   LostItem( $itemnumber, $mark_lost_from, $force_mark_returned, [$params] );
3842
3843 The final optional parameter, C<$params>, expected to contain
3844 'skip_record_index' key, which relayed down to Koha::Item/store,
3845 there it prevents calling of ModZebra index_records,
3846 which takes most of the time in batch adds/deletes: index_records better
3847 to be called later in C<additem.pl> after the whole loop.
3848
3849 $params:
3850     skip_record_index => 1|0
3851
3852 =cut
3853
3854 sub LostItem{
3855     my ($itemnumber, $mark_lost_from, $force_mark_returned, $params) = @_;
3856
3857     unless ( $mark_lost_from ) {
3858         # Temporary check to avoid regressions
3859         die q|LostItem called without $mark_lost_from, check the API.|;
3860     }
3861
3862     my $mark_returned;
3863     if ( $force_mark_returned ) {
3864         $mark_returned = 1;
3865     } else {
3866         my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3867         $mark_returned = ( $pref =~ m|$mark_lost_from| );
3868     }
3869
3870     my $dbh = C4::Context->dbh();
3871     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3872                            FROM issues 
3873                            JOIN items USING (itemnumber) 
3874                            JOIN biblio USING (biblionumber)
3875                            WHERE issues.itemnumber=?");
3876     $sth->execute($itemnumber);
3877     my $issues=$sth->fetchrow_hashref();
3878
3879     # If a borrower lost the item, add a replacement cost to the their record
3880     if ( my $borrowernumber = $issues->{borrowernumber} ){
3881         my $patron = Koha::Patrons->find( $borrowernumber );
3882
3883         my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 'LOST');
3884         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3885
3886         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3887             C4::Accounts::chargelostitem(
3888                 $borrowernumber,
3889                 $itemnumber,
3890                 $issues->{'replacementprice'},
3891                 sprintf( "%s %s %s",
3892                     $issues->{'title'}          || q{},
3893                     $issues->{'barcode'}        || q{},
3894                     $issues->{'itemcallnumber'} || q{},
3895                 ),
3896             );
3897             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3898             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3899         }
3900
3901         MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy,$params) if $mark_returned;
3902     }
3903
3904     # When an item is marked as lost, we should automatically cancel its outstanding transfers.
3905     my $item = Koha::Items->find($itemnumber);
3906     my $transfers = $item->get_transfers;
3907     while (my $transfer = $transfers->next) {
3908         $transfer->cancel({ reason => 'ItemLost', force => 1 });
3909     }
3910 }
3911
3912 sub GetOfflineOperations {
3913     my $dbh = C4::Context->dbh;
3914     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3915     $sth->execute(C4::Context->userenv->{'branch'});
3916     my $results = $sth->fetchall_arrayref({});
3917     return $results;
3918 }
3919
3920 sub GetOfflineOperation {
3921     my $operationid = shift;
3922     return unless $operationid;
3923     my $dbh = C4::Context->dbh;
3924     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3925     $sth->execute( $operationid );
3926     return $sth->fetchrow_hashref;
3927 }
3928
3929 sub AddOfflineOperation {
3930     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3931     my $dbh = C4::Context->dbh;
3932     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3933     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3934     return "Added.";
3935 }
3936
3937 sub DeleteOfflineOperation {
3938     my $dbh = C4::Context->dbh;
3939     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3940     $sth->execute( shift );
3941     return "Deleted.";
3942 }
3943
3944 sub ProcessOfflineOperation {
3945     my $operation = shift;
3946
3947     my $report;
3948     if ( $operation->{action} eq 'return' ) {
3949         $report = ProcessOfflineReturn( $operation );
3950     } elsif ( $operation->{action} eq 'issue' ) {
3951         $report = ProcessOfflineIssue( $operation );
3952     } elsif ( $operation->{action} eq 'payment' ) {
3953         $report = ProcessOfflinePayment( $operation );
3954     }
3955
3956     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3957
3958     return $report;
3959 }
3960
3961 sub ProcessOfflineReturn {
3962     my $operation = shift;
3963
3964     my $item = Koha::Items->find({barcode => $operation->{barcode}});
3965
3966     if ( $item ) {
3967         my $itemnumber = $item->itemnumber;
3968         my $issue = GetOpenIssue( $itemnumber );
3969         if ( $issue ) {
3970             my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
3971             ModDateLastSeen( $itemnumber, $leave_item_lost );
3972             MarkIssueReturned(
3973                 $issue->{borrowernumber},
3974                 $itemnumber,
3975                 $operation->{timestamp},
3976             );
3977             $item->renewals(0);
3978             $item->onloan(undef);
3979             $item->store({ log_action => 0 });
3980             return "Success.";
3981         } else {
3982             return "Item not issued.";
3983         }
3984     } else {
3985         return "Item not found.";
3986     }
3987 }
3988
3989 sub ProcessOfflineIssue {
3990     my $operation = shift;
3991
3992     my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3993
3994     if ( $patron ) {
3995         my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3996         unless ($item) {
3997             return "Barcode not found.";
3998         }
3999         my $itemnumber = $item->itemnumber;
4000         my $issue = GetOpenIssue( $itemnumber );
4001
4002         if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
4003             MarkIssueReturned(
4004                 $issue->{borrowernumber},
4005                 $itemnumber,
4006                 $operation->{timestamp},
4007             );
4008         }
4009         AddIssue(
4010             $patron->unblessed,
4011             $operation->{'barcode'},
4012             undef,
4013             1,
4014             $operation->{timestamp},
4015             undef,
4016         );
4017         return "Success.";
4018     } else {
4019         return "Borrower not found.";
4020     }
4021 }
4022
4023 sub ProcessOfflinePayment {
4024     my $operation = shift;
4025
4026     my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
4027
4028     $patron->account->pay(
4029         {
4030             amount     => $operation->{amount},
4031             library_id => $operation->{branchcode},
4032             interface  => 'koc'
4033         }
4034     );
4035
4036     return "Success.";
4037 }
4038
4039 =head2 TransferSlip
4040
4041   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
4042
4043   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
4044
4045 =cut
4046
4047 sub TransferSlip {
4048     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
4049
4050     my $item =
4051       $itemnumber
4052       ? Koha::Items->find($itemnumber)
4053       : Koha::Items->find( { barcode => $barcode } );
4054
4055     $item or return;
4056
4057     return C4::Letters::GetPreparedLetter (
4058         module => 'circulation',
4059         letter_code => 'TRANSFERSLIP',
4060         branchcode => $branch,
4061         tables => {
4062             'branches'    => $to_branch,
4063             'biblio'      => $item->biblionumber,
4064             'items'       => $item->unblessed,
4065         },
4066     );
4067 }
4068
4069 =head2 CheckIfIssuedToPatron
4070
4071   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
4072
4073   Return 1 if any record item is issued to patron, otherwise return 0
4074
4075 =cut
4076
4077 sub CheckIfIssuedToPatron {
4078     my ($borrowernumber, $biblionumber) = @_;
4079
4080     my $dbh = C4::Context->dbh;
4081     my $query = q|
4082         SELECT COUNT(*) FROM issues
4083         LEFT JOIN items ON items.itemnumber = issues.itemnumber
4084         WHERE items.biblionumber = ?
4085         AND issues.borrowernumber = ?
4086     |;
4087     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
4088     return 1 if $is_issued;
4089     return;
4090 }
4091
4092 =head2 IsItemIssued
4093
4094   IsItemIssued( $itemnumber )
4095
4096   Return 1 if the item is on loan, otherwise return 0
4097
4098 =cut
4099
4100 sub IsItemIssued {
4101     my $itemnumber = shift;
4102     my $dbh = C4::Context->dbh;
4103     my $sth = $dbh->prepare(q{
4104         SELECT COUNT(*)
4105         FROM issues
4106         WHERE itemnumber = ?
4107     });
4108     $sth->execute($itemnumber);
4109     return $sth->fetchrow;
4110 }
4111
4112 =head2 GetAgeRestriction
4113
4114   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
4115   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
4116
4117   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
4118   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
4119
4120 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
4121 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
4122 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
4123          Negative days mean the borrower has gone past the age restriction age.
4124
4125 =cut
4126
4127 sub GetAgeRestriction {
4128     my ($record_restrictions, $borrower) = @_;
4129     my $markers = C4::Context->preference('AgeRestrictionMarker');
4130
4131     return unless $record_restrictions;
4132     # Split $record_restrictions to something like FSK 16 or PEGI 6
4133     my @values = split ' ', uc($record_restrictions);
4134     return unless @values;
4135
4136     # Search first occurrence of one of the markers
4137     my @markers = split /\|/, uc($markers);
4138     return unless @markers;
4139
4140     my $index            = 0;
4141     my $restriction_year = 0;
4142     for my $value (@values) {
4143         $index++;
4144         for my $marker (@markers) {
4145             $marker =~ s/^\s+//;    #remove leading spaces
4146             $marker =~ s/\s+$//;    #remove trailing spaces
4147             if ( $marker eq $value ) {
4148                 if ( $index <= $#values ) {
4149                     $restriction_year += $values[$index];
4150                 }
4151                 last;
4152             }
4153             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
4154
4155                 # Perhaps it is something like "K16" (as in Finland)
4156                 $restriction_year += $1;
4157                 last;
4158             }
4159         }
4160         last if ( $restriction_year > 0 );
4161     }
4162
4163     #Check if the borrower is age restricted for this material and for how long.
4164     if ($restriction_year && $borrower) {
4165         if ( $borrower->{'dateofbirth'} ) {
4166             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
4167             $alloweddate[0] += $restriction_year;
4168
4169             #Prevent runime eror on leap year (invalid date)
4170             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
4171                 $alloweddate[2] = 28;
4172             }
4173
4174             #Get how many days the borrower has to reach the age restriction
4175             my @Today = split /-/, dt_from_string()->ymd();
4176             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
4177             #Negative days means the borrower went past the age restriction age
4178             return ($restriction_year, $daysToAgeRestriction);
4179         }
4180     }
4181
4182     return ($restriction_year);
4183 }
4184
4185
4186 =head2 GetPendingOnSiteCheckouts
4187
4188 =cut
4189
4190 sub GetPendingOnSiteCheckouts {
4191     my $dbh = C4::Context->dbh;
4192     return $dbh->selectall_arrayref(q|
4193         SELECT
4194           items.barcode,
4195           items.biblionumber,
4196           items.itemnumber,
4197           items.itemnotes,
4198           items.itemcallnumber,
4199           items.location,
4200           issues.date_due,
4201           issues.branchcode,
4202           issues.date_due < NOW() AS is_overdue,
4203           biblio.author,
4204           biblio.title,
4205           borrowers.firstname,
4206           borrowers.surname,
4207           borrowers.cardnumber,
4208           borrowers.borrowernumber
4209         FROM items
4210         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4211         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4212         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4213         WHERE issues.onsite_checkout = 1
4214     |, { Slice => {} } );
4215 }
4216
4217 sub GetTopIssues {
4218     my ($params) = @_;
4219
4220     my ($count, $branch, $itemtype, $ccode, $newness)
4221         = @$params{qw(count branch itemtype ccode newness)};
4222
4223     my $dbh = C4::Context->dbh;
4224     my $query = q{
4225         SELECT * FROM (
4226         SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4227           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4228           i.ccode, SUM(i.issues) AS count
4229         FROM biblio b
4230         LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4231         LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4232     };
4233
4234     my (@where_strs, @where_args);
4235
4236     if ($branch) {
4237         push @where_strs, 'i.homebranch = ?';
4238         push @where_args, $branch;
4239     }
4240     if ($itemtype) {
4241         if (C4::Context->preference('item-level_itypes')){
4242             push @where_strs, 'i.itype = ?';
4243             push @where_args, $itemtype;
4244         } else {
4245             push @where_strs, 'bi.itemtype = ?';
4246             push @where_args, $itemtype;
4247         }
4248     }
4249     if ($ccode) {
4250         push @where_strs, 'i.ccode = ?';
4251         push @where_args, $ccode;
4252     }
4253     if ($newness) {
4254         push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4255         push @where_args, $newness;
4256     }
4257
4258     if (@where_strs) {
4259         $query .= 'WHERE ' . join(' AND ', @where_strs);
4260     }
4261
4262     $query .= q{
4263         GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4264           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4265           i.ccode
4266         ORDER BY count DESC
4267     };
4268
4269     $query .= q{ ) xxx WHERE count > 0 };
4270     $count = int($count);
4271     if ($count > 0) {
4272         $query .= "LIMIT $count";
4273     }
4274
4275     my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4276
4277     return @$rows;
4278 }
4279
4280 =head2 Internal methods
4281
4282 =cut
4283
4284 sub _CalculateAndUpdateFine {
4285     my ($params) = @_;
4286
4287     my $borrower    = $params->{borrower};
4288     my $item        = $params->{item};
4289     my $issue       = $params->{issue};
4290     my $return_date = $params->{return_date};
4291
4292     unless ($borrower) { carp "No borrower passed in!" && return; }
4293     unless ($item)     { carp "No item passed in!"     && return; }
4294     unless ($issue)    { carp "No issue passed in!"    && return; }
4295
4296     my $datedue = dt_from_string( $issue->date_due );
4297
4298     # we only need to calculate and change the fines if we want to do that on return
4299     # Should be on for hourly loans
4300     my $control = C4::Context->preference('CircControl');
4301     my $control_branchcode =
4302         ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4303       : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
4304       :                                     $issue->branchcode;
4305
4306     my $date_returned = $return_date ? $return_date : dt_from_string();
4307
4308     my ( $amount, $unitcounttotal, $unitcount  ) =
4309       C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4310
4311     if ( C4::Context->preference('finesMode') eq 'production' ) {
4312         if ( $amount > 0 ) {
4313             C4::Overdues::UpdateFine({
4314                 issue_id       => $issue->issue_id,
4315                 itemnumber     => $issue->itemnumber,
4316                 borrowernumber => $issue->borrowernumber,
4317                 amount         => $amount,
4318                 due            => output_pref($datedue),
4319             });
4320         }
4321         elsif ($return_date) {
4322
4323             # Backdated returns may have fines that shouldn't exist,
4324             # so in this case, we need to drop those fines to 0
4325
4326             C4::Overdues::UpdateFine({
4327                 issue_id       => $issue->issue_id,
4328                 itemnumber     => $issue->itemnumber,
4329                 borrowernumber => $issue->borrowernumber,
4330                 amount         => 0,
4331                 due            => output_pref($datedue),
4332             });
4333         }
4334     }
4335 }
4336
4337 sub _item_denied_renewal {
4338     my ($params) = @_;
4339
4340     my $item = $params->{item};
4341     return unless $item;
4342
4343     my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4344     return unless $denyingrules;
4345     foreach my $field (keys %$denyingrules) {
4346         my $val = $item->$field;
4347         if( !defined $val) {
4348             if ( any { !defined $_ }  @{$denyingrules->{$field}} ){
4349                 return 1;
4350             }
4351         } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4352            # If the results matches the values in the syspref
4353            # We return true if match found
4354             return 1;
4355         }
4356     }
4357     return 0;
4358 }
4359
4360 1;
4361
4362 __END__
4363
4364 =head1 AUTHOR
4365
4366 Koha Development Team <http://koha-community.org/>
4367
4368 =cut