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