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