Bug 29476: (follow-up) Fix return, add precision test
[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
3267     if ( defined $issuing_rule->{norenewalbefore}
3268         and $issuing_rule->{norenewalbefore} ne "" )
3269     {
3270         my $soonestrenewal =
3271           dt_from_string( $itemissue->date_due )->subtract(
3272             $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
3273
3274         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3275             and $issuing_rule->{lengthunit} eq 'days' )
3276         {
3277             $soonestrenewal->truncate( to => 'day' );
3278         }
3279         return $soonestrenewal if $now < $soonestrenewal;
3280     } elsif ( $itemissue->auto_renew && $patron->autorenew_checkouts ) {
3281         # Checkouts with auto-renewing fall back to due date
3282         my $soonestrenewal = dt_from_string( $itemissue->date_due );
3283         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3284             and $issuing_rule->{lengthunit} eq 'days' )
3285         {
3286             $soonestrenewal->truncate( to => 'day' );
3287         }
3288         return $soonestrenewal;
3289     }
3290     return $now;
3291 }
3292
3293 =head2 GetLatestAutoRenewDate
3294
3295   $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3296
3297 Find out the latest possible auto renew date of a borrowed item.
3298
3299 C<$borrowernumber> is the borrower number of the patron who currently
3300 has the item on loan.
3301
3302 C<$itemnumber> is the number of the item to renew.
3303
3304 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3305 auto renew date, based on the value "No auto renewal after" and the "No auto
3306 renewal after (hard limit) of the applicable issuing rule.
3307 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3308 or item cannot be found.
3309
3310 =cut
3311
3312 sub GetLatestAutoRenewDate {
3313     my ( $borrowernumber, $itemnumber ) = @_;
3314
3315     my $dbh = C4::Context->dbh;
3316
3317     my $item      = Koha::Items->find($itemnumber)  or return;
3318     my $itemissue = $item->checkout                 or return;
3319
3320     $borrowernumber ||= $itemissue->borrowernumber;
3321     my $patron = Koha::Patrons->find( $borrowernumber )
3322       or return;
3323
3324     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3325     my $circulation_rules = Koha::CirculationRules->get_effective_rules(
3326         {
3327             categorycode => $patron->categorycode,
3328             itemtype     => $item->effective_itemtype,
3329             branchcode   => $branchcode,
3330             rules => [
3331                 'no_auto_renewal_after',
3332                 'no_auto_renewal_after_hard_limit',
3333                 'lengthunit',
3334             ]
3335         }
3336     );
3337
3338     return unless $circulation_rules;
3339     return
3340       if ( not $circulation_rules->{no_auto_renewal_after}
3341             or $circulation_rules->{no_auto_renewal_after} eq '' )
3342       and ( not $circulation_rules->{no_auto_renewal_after_hard_limit}
3343              or $circulation_rules->{no_auto_renewal_after_hard_limit} eq '' );
3344
3345     my $maximum_renewal_date;
3346     if ( $circulation_rules->{no_auto_renewal_after} ) {
3347         $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3348         $maximum_renewal_date->add(
3349             $circulation_rules->{lengthunit} => $circulation_rules->{no_auto_renewal_after}
3350         );
3351     }
3352
3353     if ( $circulation_rules->{no_auto_renewal_after_hard_limit} ) {
3354         my $dt = dt_from_string( $circulation_rules->{no_auto_renewal_after_hard_limit} );
3355         $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3356     }
3357     return $maximum_renewal_date;
3358 }
3359
3360
3361 =head2 GetIssuingCharges
3362
3363   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3364
3365 Calculate how much it would cost for a given patron to borrow a given
3366 item, including any applicable discounts.
3367
3368 C<$itemnumber> is the item number of item the patron wishes to borrow.
3369
3370 C<$borrowernumber> is the patron's borrower number.
3371
3372 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3373 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3374 if it's a video).
3375
3376 =cut
3377
3378 sub GetIssuingCharges {
3379
3380     # calculate charges due
3381     my ( $itemnumber, $borrowernumber ) = @_;
3382     my $charge = 0;
3383     my $dbh    = C4::Context->dbh;
3384     my $item_type;
3385
3386     # Get the book's item type and rental charge (via its biblioitem).
3387     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3388         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3389     $charge_query .= (C4::Context->preference('item-level_itypes'))
3390         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3391         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3392
3393     $charge_query .= ' WHERE items.itemnumber =?';
3394
3395     my $sth = $dbh->prepare($charge_query);
3396     $sth->execute($itemnumber);
3397     if ( my $item_data = $sth->fetchrow_hashref ) {
3398         $item_type = $item_data->{itemtype};
3399         $charge    = $item_data->{rentalcharge};
3400         if ($charge) {
3401             # FIXME This should follow CircControl
3402             my $branch = C4::Context::mybranch();
3403             my $patron = Koha::Patrons->find( $borrowernumber );
3404             my $discount = Koha::CirculationRules->get_effective_rule({
3405                 categorycode => $patron->categorycode,
3406                 branchcode   => $branch,
3407                 itemtype     => $item_type,
3408                 rule_name    => 'rentaldiscount'
3409             });
3410             if ($discount) {
3411                 $charge = ( $charge * ( 100 - $discount->rule_value ) ) / 100;
3412             }
3413             $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3414         }
3415     }
3416
3417     return ( $charge, $item_type );
3418 }
3419
3420 =head2 AddIssuingCharge
3421
3422   &AddIssuingCharge( $checkout, $charge, $type )
3423
3424 =cut
3425
3426 sub AddIssuingCharge {
3427     my ( $checkout, $charge, $type ) = @_;
3428
3429     # FIXME What if checkout does not exist?
3430
3431     my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3432     my $accountline = $account->add_debit(
3433         {
3434             amount      => $charge,
3435             note        => undef,
3436             user_id     => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
3437             library_id  => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3438             interface   => C4::Context->interface,
3439             type        => $type,
3440             item_id     => $checkout->itemnumber,
3441             issue_id    => $checkout->issue_id,
3442         }
3443     );
3444 }
3445
3446 =head2 GetTransfers
3447
3448   GetTransfers($itemnumber);
3449
3450 =cut
3451
3452 sub GetTransfers {
3453     my ($itemnumber) = @_;
3454
3455     my $dbh = C4::Context->dbh;
3456
3457     my $query = '
3458         SELECT datesent,
3459                frombranch,
3460                tobranch,
3461                branchtransfer_id,
3462                daterequested,
3463                reason
3464         FROM branchtransfers
3465         WHERE itemnumber = ?
3466           AND datearrived IS NULL
3467           AND datecancelled IS NULL
3468         ';
3469     my $sth = $dbh->prepare($query);
3470     $sth->execute($itemnumber);
3471     my @row = $sth->fetchrow_array();
3472     return @row;
3473 }
3474
3475 =head2 GetTransfersFromTo
3476
3477   @results = GetTransfersFromTo($frombranch,$tobranch);
3478
3479 Returns the list of pending transfers between $from and $to branch
3480
3481 =cut
3482
3483 sub GetTransfersFromTo {
3484     my ( $frombranch, $tobranch ) = @_;
3485     return unless ( $frombranch && $tobranch );
3486     my $dbh   = C4::Context->dbh;
3487     my $query = "
3488         SELECT branchtransfer_id,itemnumber,datesent,frombranch
3489         FROM   branchtransfers
3490         WHERE  frombranch=?
3491           AND  tobranch=?
3492           AND datecancelled IS NULL
3493           AND datesent IS NOT NULL
3494           AND datearrived IS NULL
3495     ";
3496     my $sth = $dbh->prepare($query);
3497     $sth->execute( $frombranch, $tobranch );
3498     my @gettransfers;
3499
3500     while ( my $data = $sth->fetchrow_hashref ) {
3501         push @gettransfers, $data;
3502     }
3503     return (@gettransfers);
3504 }
3505
3506 =head2 SendCirculationAlert
3507
3508 Send out a C<check-in> or C<checkout> alert using the messaging system.
3509
3510 B<Parameters>:
3511
3512 =over 4
3513
3514 =item type
3515
3516 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3517
3518 =item item
3519
3520 Hashref of information about the item being checked in or out.
3521
3522 =item borrower
3523
3524 Hashref of information about the borrower of the item.
3525
3526 =item branch
3527
3528 The branchcode from where the checkout or check-in took place.
3529
3530 =back
3531
3532 B<Example>:
3533
3534     SendCirculationAlert({
3535         type     => 'CHECKOUT',
3536         item     => $item,
3537         borrower => $borrower,
3538         branch   => $branch,
3539     });
3540
3541 =cut
3542
3543 sub SendCirculationAlert {
3544     my ($opts) = @_;
3545     my ($type, $item, $borrower, $branch, $issue) =
3546         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch}, $opts->{issue});
3547     my %message_name = (
3548         CHECKIN  => 'Item_Check_in',
3549         CHECKOUT => 'Item_Checkout',
3550         RENEWAL  => 'Item_Checkout',
3551     );
3552     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3553         borrowernumber => $borrower->{borrowernumber},
3554         message_name   => $message_name{$type},
3555     });
3556
3557
3558     my $tables = {
3559         items => $item->{itemnumber},
3560         biblio      => $item->{biblionumber},
3561         biblioitems => $item->{biblionumber},
3562         borrowers   => $borrower,
3563         branches    => $branch,
3564     };
3565
3566     # TODO: Currently, we need to pass an issue_id as identifier for old_issues, but still an itemnumber for issues.
3567     # See C4::Letters:: _parseletter_sth
3568     if( $type eq 'CHECKIN' ){
3569         $tables->{old_issues} = $issue->issue_id;
3570     } else {
3571         $tables->{issues} = $item->{itemnumber};
3572     }
3573
3574     my $schema = Koha::Database->new->schema;
3575     my @transports = keys %{ $borrower_preferences->{transports} };
3576
3577     # From the MySQL doc:
3578     # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3579     # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3580     # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3581     my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_TESTING};
3582
3583     for my $mtt (@transports) {
3584         my $letter =  C4::Letters::GetPreparedLetter (
3585             module => 'circulation',
3586             letter_code => $type,
3587             branchcode => $branch,
3588             message_transport_type => $mtt,
3589             lang => $borrower->{lang},
3590             tables => $tables,
3591         ) or next;
3592
3593         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3594         C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3595         my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3596         unless ( $message ) {
3597             C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3598             C4::Message->enqueue($letter, $borrower, $mtt);
3599         } else {
3600             $message->append($letter);
3601             $message->update;
3602         }
3603         C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3604     }
3605
3606     return;
3607 }
3608
3609 =head2 updateWrongTransfer
3610
3611   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3612
3613 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 
3614
3615 =cut
3616
3617 sub updateWrongTransfer {
3618         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3619
3620     # first step: cancel the original transfer
3621     my $item = Koha::Items->find($itemNumber);
3622     my $transfer = $item->get_transfer;
3623     $transfer->set({ datecancelled => dt_from_string, cancellation_reason => 'WrongTransfer' })->store();
3624
3625     # second step: create a new transfer to the right location
3626     my $new_transfer = $item->request_transfer(
3627         {
3628             to            => $transfer->to_library,
3629             reason        => $transfer->reason,
3630             comment       => $transfer->comments,
3631             ignore_limits => 1,
3632             enqueue       => 1
3633         }
3634     );
3635
3636     return $new_transfer;
3637 }
3638
3639 =head2 CalcDateDue
3640
3641 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3642
3643 this function calculates the due date given the start date and configured circulation rules,
3644 checking against the holidays calendar as per the daysmode circulation rule.
3645 C<$startdate>   = DateTime object representing start date of loan period (assumed to be today)
3646 C<$itemtype>  = itemtype code of item in question
3647 C<$branch>  = location whose calendar to use
3648 C<$borrower> = Borrower object
3649 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3650
3651 =cut
3652
3653 sub CalcDateDue {
3654     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3655
3656     $isrenewal ||= 0;
3657
3658     # loanlength now a href
3659     my $loanlength =
3660             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3661
3662     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3663             ? qq{renewalperiod}
3664             : qq{issuelength};
3665
3666     my $datedue;
3667     if ( $startdate ) {
3668         if (ref $startdate ne 'DateTime' ) {
3669             $datedue = dt_from_string($datedue);
3670         } else {
3671             $datedue = $startdate->clone;
3672         }
3673     } else {
3674         $datedue = dt_from_string()->truncate( to => 'minute' );
3675     }
3676
3677
3678     my $daysmode = Koha::CirculationRules->get_effective_daysmode(
3679         {
3680             categorycode => $borrower->{categorycode},
3681             itemtype     => $itemtype,
3682             branchcode   => $branch,
3683         }
3684     );
3685
3686     # calculate the datedue as normal
3687     if ( $daysmode eq 'Days' )
3688     {    # ignoring calendar
3689         if ( $loanlength->{lengthunit} eq 'hours' ) {
3690             $datedue->add( hours => $loanlength->{$length_key} );
3691         } else {    # days
3692             $datedue->add( days => $loanlength->{$length_key} );
3693             $datedue->set_hour(23);
3694             $datedue->set_minute(59);
3695         }
3696     } else {
3697         my $dur;
3698         if ($loanlength->{lengthunit} eq 'hours') {
3699             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3700         }
3701         else { # days
3702             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3703         }
3704         my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3705         $datedue = $calendar->addDuration( $datedue, $dur, $loanlength->{lengthunit} );
3706         if ($loanlength->{lengthunit} eq 'days') {
3707             $datedue->set_hour(23);
3708             $datedue->set_minute(59);
3709         }
3710     }
3711
3712     # if Hard Due Dates are used, retrieve them and apply as necessary
3713     my ( $hardduedate, $hardduedatecompare ) =
3714       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3715     if ($hardduedate) {    # hardduedates are currently dates
3716         $hardduedate->truncate( to => 'minute' );
3717         $hardduedate->set_hour(23);
3718         $hardduedate->set_minute(59);
3719         my $cmp = DateTime->compare( $hardduedate, $datedue );
3720
3721 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3722 # if the calculated date is before the 'after' Hard Due Date (floor), override
3723 # if the hard due date is set to 'exactly', overrride
3724         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3725             $datedue = $hardduedate->clone;
3726         }
3727
3728         # in all other cases, keep the date due as it is
3729
3730     }
3731
3732     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3733     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3734         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3735         if( $expiry_dt ) { #skip empty expiry date..
3736             $expiry_dt->set( hour => 23, minute => 59);
3737             my $d1= $datedue->clone->set_time_zone('floating');
3738             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3739                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3740             }
3741         }
3742         if ( $daysmode ne 'Days' ) {
3743           my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3744           if ( $calendar->is_holiday($datedue) ) {
3745               # Don't return on a closed day
3746               $datedue = $calendar->prev_open_days( $datedue, 1 );
3747           }
3748         }
3749     }
3750
3751     return $datedue;
3752 }
3753
3754
3755 sub CheckValidBarcode{
3756 my ($barcode) = @_;
3757 my $dbh = C4::Context->dbh;
3758 my $query=qq|SELECT count(*) 
3759              FROM items 
3760              WHERE barcode=?
3761             |;
3762 my $sth = $dbh->prepare($query);
3763 $sth->execute($barcode);
3764 my $exist=$sth->fetchrow ;
3765 return $exist;
3766 }
3767
3768 =head2 IsBranchTransferAllowed
3769
3770   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3771
3772 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3773
3774 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3775 Koha::Item->can_be_transferred.
3776
3777 =cut
3778
3779 sub IsBranchTransferAllowed {
3780         my ( $toBranch, $fromBranch, $code ) = @_;
3781
3782         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3783         
3784         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3785         my $dbh = C4::Context->dbh;
3786             
3787         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3788         $sth->execute( $toBranch, $fromBranch, $code );
3789         my $limit = $sth->fetchrow_hashref();
3790                         
3791         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3792         if ( $limit->{'limitId'} ) {
3793                 return 0;
3794         } else {
3795                 return 1;
3796         }
3797 }                                                        
3798
3799 =head2 CreateBranchTransferLimit
3800
3801   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3802
3803 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3804
3805 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3806
3807 =cut
3808
3809 sub CreateBranchTransferLimit {
3810    my ( $toBranch, $fromBranch, $code ) = @_;
3811    return unless defined($toBranch) && defined($fromBranch);
3812    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3813    
3814    my $dbh = C4::Context->dbh;
3815    
3816    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3817    return $sth->execute( $code, $toBranch, $fromBranch );
3818 }
3819
3820 =head2 DeleteBranchTransferLimits
3821
3822     my $result = DeleteBranchTransferLimits($frombranch);
3823
3824 Deletes all the library transfer limits for one library.  Returns the
3825 number of limits deleted, 0e0 if no limits were deleted, or undef if
3826 no arguments are supplied.
3827
3828 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3829     fromBranch => $fromBranch
3830     })->delete.
3831
3832 =cut
3833
3834 sub DeleteBranchTransferLimits {
3835     my $branch = shift;
3836     return unless defined $branch;
3837     my $dbh    = C4::Context->dbh;
3838     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3839     return $sth->execute($branch);
3840 }
3841
3842 sub ReturnLostItem{
3843     my ( $borrowernumber, $itemnum ) = @_;
3844     MarkIssueReturned( $borrowernumber, $itemnum );
3845 }
3846
3847 =head2 LostItem
3848
3849   LostItem( $itemnumber, $mark_lost_from, $force_mark_returned, [$params] );
3850
3851 The final optional parameter, C<$params>, expected to contain
3852 'skip_record_index' key, which relayed down to Koha::Item/store,
3853 there it prevents calling of ModZebra index_records,
3854 which takes most of the time in batch adds/deletes: index_records better
3855 to be called later in C<additem.pl> after the whole loop.
3856
3857 $params:
3858     skip_record_index => 1|0
3859
3860 =cut
3861
3862 sub LostItem{
3863     my ($itemnumber, $mark_lost_from, $force_mark_returned, $params) = @_;
3864
3865     unless ( $mark_lost_from ) {
3866         # Temporary check to avoid regressions
3867         die q|LostItem called without $mark_lost_from, check the API.|;
3868     }
3869
3870     my $mark_returned;
3871     if ( $force_mark_returned ) {
3872         $mark_returned = 1;
3873     } else {
3874         my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3875         $mark_returned = ( $pref =~ m|$mark_lost_from| );
3876     }
3877
3878     my $dbh = C4::Context->dbh();
3879     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3880                            FROM issues 
3881                            JOIN items USING (itemnumber) 
3882                            JOIN biblio USING (biblionumber)
3883                            WHERE issues.itemnumber=?");
3884     $sth->execute($itemnumber);
3885     my $issues=$sth->fetchrow_hashref();
3886
3887     # If a borrower lost the item, add a replacement cost to the their record
3888     if ( my $borrowernumber = $issues->{borrowernumber} ){
3889         my $patron = Koha::Patrons->find( $borrowernumber );
3890
3891         my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 'LOST');
3892         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3893
3894         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3895             C4::Accounts::chargelostitem(
3896                 $borrowernumber,
3897                 $itemnumber,
3898                 $issues->{'replacementprice'},
3899                 sprintf( "%s %s %s",
3900                     $issues->{'title'}          || q{},
3901                     $issues->{'barcode'}        || q{},
3902                     $issues->{'itemcallnumber'} || q{},
3903                 ),
3904             );
3905             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3906             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3907         }
3908
3909         MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy,$params) if $mark_returned;
3910     }
3911
3912     # When an item is marked as lost, we should automatically cancel its outstanding transfers.
3913     my $item = Koha::Items->find($itemnumber);
3914     my $transfers = $item->get_transfers;
3915     while (my $transfer = $transfers->next) {
3916         $transfer->cancel({ reason => 'ItemLost', force => 1 });
3917     }
3918 }
3919
3920 sub GetOfflineOperations {
3921     my $dbh = C4::Context->dbh;
3922     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3923     $sth->execute(C4::Context->userenv->{'branch'});
3924     my $results = $sth->fetchall_arrayref({});
3925     return $results;
3926 }
3927
3928 sub GetOfflineOperation {
3929     my $operationid = shift;
3930     return unless $operationid;
3931     my $dbh = C4::Context->dbh;
3932     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3933     $sth->execute( $operationid );
3934     return $sth->fetchrow_hashref;
3935 }
3936
3937 sub AddOfflineOperation {
3938     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3939     my $dbh = C4::Context->dbh;
3940     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3941     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3942     return "Added.";
3943 }
3944
3945 sub DeleteOfflineOperation {
3946     my $dbh = C4::Context->dbh;
3947     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3948     $sth->execute( shift );
3949     return "Deleted.";
3950 }
3951
3952 sub ProcessOfflineOperation {
3953     my $operation = shift;
3954
3955     my $report;
3956     if ( $operation->{action} eq 'return' ) {
3957         $report = ProcessOfflineReturn( $operation );
3958     } elsif ( $operation->{action} eq 'issue' ) {
3959         $report = ProcessOfflineIssue( $operation );
3960     } elsif ( $operation->{action} eq 'payment' ) {
3961         $report = ProcessOfflinePayment( $operation );
3962     }
3963
3964     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3965
3966     return $report;
3967 }
3968
3969 sub ProcessOfflineReturn {
3970     my $operation = shift;
3971
3972     my $item = Koha::Items->find({barcode => $operation->{barcode}});
3973
3974     if ( $item ) {
3975         my $itemnumber = $item->itemnumber;
3976         my $issue = GetOpenIssue( $itemnumber );
3977         if ( $issue ) {
3978             my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
3979             ModDateLastSeen( $itemnumber, $leave_item_lost );
3980             MarkIssueReturned(
3981                 $issue->{borrowernumber},
3982                 $itemnumber,
3983                 $operation->{timestamp},
3984             );
3985             $item->renewals(0);
3986             $item->onloan(undef);
3987             $item->store({ log_action => 0 });
3988             return "Success.";
3989         } else {
3990             return "Item not issued.";
3991         }
3992     } else {
3993         return "Item not found.";
3994     }
3995 }
3996
3997 sub ProcessOfflineIssue {
3998     my $operation = shift;
3999
4000     my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
4001
4002     if ( $patron ) {
4003         my $item = Koha::Items->find({ barcode => $operation->{barcode} });
4004         unless ($item) {
4005             return "Barcode not found.";
4006         }
4007         my $itemnumber = $item->itemnumber;
4008         my $issue = GetOpenIssue( $itemnumber );
4009
4010         if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
4011             MarkIssueReturned(
4012                 $issue->{borrowernumber},
4013                 $itemnumber,
4014                 $operation->{timestamp},
4015             );
4016         }
4017         AddIssue(
4018             $patron->unblessed,
4019             $operation->{'barcode'},
4020             undef,
4021             1,
4022             $operation->{timestamp},
4023             undef,
4024         );
4025         return "Success.";
4026     } else {
4027         return "Borrower not found.";
4028     }
4029 }
4030
4031 sub ProcessOfflinePayment {
4032     my $operation = shift;
4033
4034     my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
4035
4036     $patron->account->pay(
4037         {
4038             amount     => $operation->{amount},
4039             library_id => $operation->{branchcode},
4040             interface  => 'koc'
4041         }
4042     );
4043
4044     return "Success.";
4045 }
4046
4047 =head2 TransferSlip
4048
4049   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
4050
4051   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
4052
4053 =cut
4054
4055 sub TransferSlip {
4056     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
4057
4058     my $item =
4059       $itemnumber
4060       ? Koha::Items->find($itemnumber)
4061       : Koha::Items->find( { barcode => $barcode } );
4062
4063     $item or return;
4064
4065     return C4::Letters::GetPreparedLetter (
4066         module => 'circulation',
4067         letter_code => 'TRANSFERSLIP',
4068         branchcode => $branch,
4069         tables => {
4070             'branches'    => $to_branch,
4071             'biblio'      => $item->biblionumber,
4072             'items'       => $item->unblessed,
4073         },
4074     );
4075 }
4076
4077 =head2 CheckIfIssuedToPatron
4078
4079   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
4080
4081   Return 1 if any record item is issued to patron, otherwise return 0
4082
4083 =cut
4084
4085 sub CheckIfIssuedToPatron {
4086     my ($borrowernumber, $biblionumber) = @_;
4087
4088     my $dbh = C4::Context->dbh;
4089     my $query = q|
4090         SELECT COUNT(*) FROM issues
4091         LEFT JOIN items ON items.itemnumber = issues.itemnumber
4092         WHERE items.biblionumber = ?
4093         AND issues.borrowernumber = ?
4094     |;
4095     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
4096     return 1 if $is_issued;
4097     return;
4098 }
4099
4100 =head2 IsItemIssued
4101
4102   IsItemIssued( $itemnumber )
4103
4104   Return 1 if the item is on loan, otherwise return 0
4105
4106 =cut
4107
4108 sub IsItemIssued {
4109     my $itemnumber = shift;
4110     my $dbh = C4::Context->dbh;
4111     my $sth = $dbh->prepare(q{
4112         SELECT COUNT(*)
4113         FROM issues
4114         WHERE itemnumber = ?
4115     });
4116     $sth->execute($itemnumber);
4117     return $sth->fetchrow;
4118 }
4119
4120 =head2 GetAgeRestriction
4121
4122   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
4123   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
4124
4125   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
4126   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
4127
4128 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
4129 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
4130 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
4131          Negative days mean the borrower has gone past the age restriction age.
4132
4133 =cut
4134
4135 sub GetAgeRestriction {
4136     my ($record_restrictions, $borrower) = @_;
4137     my $markers = C4::Context->preference('AgeRestrictionMarker');
4138
4139     return unless $record_restrictions;
4140     # Split $record_restrictions to something like FSK 16 or PEGI 6
4141     my @values = split ' ', uc($record_restrictions);
4142     return unless @values;
4143
4144     # Search first occurrence of one of the markers
4145     my @markers = split /\|/, uc($markers);
4146     return unless @markers;
4147
4148     my $index            = 0;
4149     my $restriction_year = 0;
4150     for my $value (@values) {
4151         $index++;
4152         for my $marker (@markers) {
4153             $marker =~ s/^\s+//;    #remove leading spaces
4154             $marker =~ s/\s+$//;    #remove trailing spaces
4155             if ( $marker eq $value ) {
4156                 if ( $index <= $#values ) {
4157                     $restriction_year += $values[$index];
4158                 }
4159                 last;
4160             }
4161             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
4162
4163                 # Perhaps it is something like "K16" (as in Finland)
4164                 $restriction_year += $1;
4165                 last;
4166             }
4167         }
4168         last if ( $restriction_year > 0 );
4169     }
4170
4171     #Check if the borrower is age restricted for this material and for how long.
4172     if ($restriction_year && $borrower) {
4173         if ( $borrower->{'dateofbirth'} ) {
4174             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
4175             $alloweddate[0] += $restriction_year;
4176
4177             #Prevent runime eror on leap year (invalid date)
4178             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
4179                 $alloweddate[2] = 28;
4180             }
4181
4182             #Get how many days the borrower has to reach the age restriction
4183             my @Today = split /-/, dt_from_string()->ymd();
4184             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
4185             #Negative days means the borrower went past the age restriction age
4186             return ($restriction_year, $daysToAgeRestriction);
4187         }
4188     }
4189
4190     return ($restriction_year);
4191 }
4192
4193
4194 =head2 GetPendingOnSiteCheckouts
4195
4196 =cut
4197
4198 sub GetPendingOnSiteCheckouts {
4199     my $dbh = C4::Context->dbh;
4200     return $dbh->selectall_arrayref(q|
4201         SELECT
4202           items.barcode,
4203           items.biblionumber,
4204           items.itemnumber,
4205           items.itemnotes,
4206           items.itemcallnumber,
4207           items.location,
4208           issues.date_due,
4209           issues.branchcode,
4210           issues.date_due < NOW() AS is_overdue,
4211           biblio.author,
4212           biblio.title,
4213           borrowers.firstname,
4214           borrowers.surname,
4215           borrowers.cardnumber,
4216           borrowers.borrowernumber
4217         FROM items
4218         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4219         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4220         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4221         WHERE issues.onsite_checkout = 1
4222     |, { Slice => {} } );
4223 }
4224
4225 sub GetTopIssues {
4226     my ($params) = @_;
4227
4228     my ($count, $branch, $itemtype, $ccode, $newness)
4229         = @$params{qw(count branch itemtype ccode newness)};
4230
4231     my $dbh = C4::Context->dbh;
4232     my $query = q{
4233         SELECT * FROM (
4234         SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4235           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4236           i.ccode, SUM(i.issues) AS count
4237         FROM biblio b
4238         LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4239         LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4240     };
4241
4242     my (@where_strs, @where_args);
4243
4244     if ($branch) {
4245         push @where_strs, 'i.homebranch = ?';
4246         push @where_args, $branch;
4247     }
4248     if ($itemtype) {
4249         if (C4::Context->preference('item-level_itypes')){
4250             push @where_strs, 'i.itype = ?';
4251             push @where_args, $itemtype;
4252         } else {
4253             push @where_strs, 'bi.itemtype = ?';
4254             push @where_args, $itemtype;
4255         }
4256     }
4257     if ($ccode) {
4258         push @where_strs, 'i.ccode = ?';
4259         push @where_args, $ccode;
4260     }
4261     if ($newness) {
4262         push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4263         push @where_args, $newness;
4264     }
4265
4266     if (@where_strs) {
4267         $query .= 'WHERE ' . join(' AND ', @where_strs);
4268     }
4269
4270     $query .= q{
4271         GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4272           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4273           i.ccode
4274         ORDER BY count DESC
4275     };
4276
4277     $query .= q{ ) xxx WHERE count > 0 };
4278     $count = int($count);
4279     if ($count > 0) {
4280         $query .= "LIMIT $count";
4281     }
4282
4283     my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4284
4285     return @$rows;
4286 }
4287
4288 =head2 Internal methods
4289
4290 =cut
4291
4292 sub _CalculateAndUpdateFine {
4293     my ($params) = @_;
4294
4295     my $borrower    = $params->{borrower};
4296     my $item        = $params->{item};
4297     my $issue       = $params->{issue};
4298     my $return_date = $params->{return_date};
4299
4300     unless ($borrower) { carp "No borrower passed in!" && return; }
4301     unless ($item)     { carp "No item passed in!"     && return; }
4302     unless ($issue)    { carp "No issue passed in!"    && return; }
4303
4304     my $datedue = dt_from_string( $issue->date_due );
4305
4306     # we only need to calculate and change the fines if we want to do that on return
4307     # Should be on for hourly loans
4308     my $control = C4::Context->preference('CircControl');
4309     my $control_branchcode =
4310         ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4311       : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
4312       :                                     $issue->branchcode;
4313
4314     my $date_returned = $return_date ? $return_date : dt_from_string();
4315
4316     my ( $amount, $unitcounttotal, $unitcount  ) =
4317       C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4318
4319     if ( C4::Context->preference('finesMode') eq 'production' ) {
4320         if ( $amount > 0 ) {
4321             C4::Overdues::UpdateFine({
4322                 issue_id       => $issue->issue_id,
4323                 itemnumber     => $issue->itemnumber,
4324                 borrowernumber => $issue->borrowernumber,
4325                 amount         => $amount,
4326                 due            => output_pref($datedue),
4327             });
4328         }
4329         elsif ($return_date) {
4330
4331             # Backdated returns may have fines that shouldn't exist,
4332             # so in this case, we need to drop those fines to 0
4333
4334             C4::Overdues::UpdateFine({
4335                 issue_id       => $issue->issue_id,
4336                 itemnumber     => $issue->itemnumber,
4337                 borrowernumber => $issue->borrowernumber,
4338                 amount         => 0,
4339                 due            => output_pref($datedue),
4340             });
4341         }
4342     }
4343 }
4344
4345 sub _item_denied_renewal {
4346     my ($params) = @_;
4347
4348     my $item = $params->{item};
4349     return unless $item;
4350
4351     my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4352     return unless $denyingrules;
4353     foreach my $field (keys %$denyingrules) {
4354         my $val = $item->$field;
4355         if( !defined $val) {
4356             if ( any { !defined $_ }  @{$denyingrules->{$field}} ){
4357                 return 1;
4358             }
4359         } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4360            # If the results matches the values in the syspref
4361            # We return true if match found
4362             return 1;
4363         }
4364     }
4365     return 0;
4366 }
4367
4368 1;
4369
4370 __END__
4371
4372 =head1 AUTHOR
4373
4374 Koha Development Team <http://koha-community.org/>
4375
4376 =cut