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