Bug 27808: Refresh the item object when AddReturn is called
[koha.git] / C4 / Circulation.pm
1 package C4::Circulation;
2
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22 use DateTime;
23 use POSIX qw( floor );
24 use YAML::XS;
25 use Encode;
26
27 use Koha::DateUtils;
28 use C4::Context;
29 use C4::Stats;
30 use C4::Reserves;
31 use C4::Biblio;
32 use C4::Items;
33 use C4::Members;
34 use C4::Accounts;
35 use C4::ItemCirculationAlertPreference;
36 use C4::Message;
37 use C4::Debug;
38 use C4::Log; # logaction
39 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
40 use C4::RotatingCollections qw(GetCollectionItemBranches);
41 use Algorithm::CheckDigits;
42
43 use Data::Dumper;
44 use Koha::Account;
45 use Koha::AuthorisedValues;
46 use Koha::Biblioitems;
47 use Koha::DateUtils;
48 use Koha::Calendar;
49 use Koha::Checkouts;
50 use Koha::Illrequests;
51 use Koha::Items;
52 use Koha::Patrons;
53 use Koha::Patron::Debarments;
54 use Koha::Database;
55 use Koha::Libraries;
56 use Koha::Account::Lines;
57 use Koha::Holds;
58 use Koha::Account::Lines;
59 use Koha::Account::Offsets;
60 use Koha::Config::SysPrefs;
61 use Koha::Charges::Fees;
62 use Koha::Config::SysPref;
63 use Koha::Checkouts::ReturnClaims;
64 use Koha::SearchEngine::Indexer;
65 use Koha::Exceptions::Checkout;
66 use Carp;
67 use List::MoreUtils qw( uniq any );
68 use Scalar::Util qw( looks_like_number );
69 use Try::Tiny;
70 use Date::Calc qw(
71   Today
72   Today_and_Now
73   Add_Delta_YM
74   Add_Delta_DHMS
75   Date_to_Days
76   Day_of_Week
77   Add_Delta_Days
78 );
79 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
80
81 BEGIN {
82         require Exporter;
83         @ISA    = qw(Exporter);
84
85         # FIXME subs that should probably be elsewhere
86         push @EXPORT, qw(
87                 &barcodedecode
88         &LostItem
89         &ReturnLostItem
90         &GetPendingOnSiteCheckouts
91         );
92
93         # subs to deal with issuing a book
94         push @EXPORT, qw(
95                 &CanBookBeIssued
96                 &CanBookBeRenewed
97                 &AddIssue
98                 &AddRenewal
99                 &GetRenewCount
100         &GetSoonestRenewDate
101         &GetLatestAutoRenewDate
102                 &GetIssuingCharges
103         &GetBranchBorrowerCircRule
104         &GetBranchItemRule
105                 &GetOpenIssue
106         &CheckIfIssuedToPatron
107         &IsItemIssued
108         GetTopIssues
109         );
110
111         # subs to deal with returns
112         push @EXPORT, qw(
113                 &AddReturn
114         &MarkIssueReturned
115         );
116
117         # subs to deal with transfers
118         push @EXPORT, qw(
119                 &transferbook
120                 &GetTransfers
121                 &GetTransfersFromTo
122                 &updateWrongTransfer
123                 &DeleteTransfer
124                 &IsBranchTransferAllowed
125                 &CreateBranchTransferLimit
126                 &DeleteBranchTransferLimits
127         &TransferSlip
128         );
129
130     # subs to deal with offline circulation
131     push @EXPORT, qw(
132       &GetOfflineOperations
133       &GetOfflineOperation
134       &AddOfflineOperation
135       &DeleteOfflineOperation
136       &ProcessOfflineOperation
137     );
138 }
139
140 =head1 NAME
141
142 C4::Circulation - Koha circulation module
143
144 =head1 SYNOPSIS
145
146 use C4::Circulation;
147
148 =head1 DESCRIPTION
149
150 The functions in this module deal with circulation, issues, and
151 returns, as well as general information about the library.
152 Also deals with inventory.
153
154 =head1 FUNCTIONS
155
156 =head2 barcodedecode
157
158   $str = &barcodedecode($barcode, [$filter]);
159
160 Generic filter function for barcode string.
161 Called on every circ if the System Pref itemBarcodeInputFilter is set.
162 Will do some manipulation of the barcode for systems that deliver a barcode
163 to circulation.pl that differs from the barcode stored for the item.
164 For proper functioning of this filter, calling the function on the 
165 correct barcode string (items.barcode) should return an unaltered barcode.
166
167 The optional $filter argument is to allow for testing or explicit 
168 behavior that ignores the System Pref.  Valid values are the same as the 
169 System Pref options.
170
171 =cut
172
173 # FIXME -- the &decode fcn below should be wrapped into this one.
174 # FIXME -- these plugins should be moved out of Circulation.pm
175 #
176 sub barcodedecode {
177     my ($barcode, $filter) = @_;
178     my $branch = C4::Context::mybranch();
179     $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
180     $filter or return $barcode;     # ensure filter is defined, else return untouched barcode
181         if ($filter eq 'whitespace') {
182                 $barcode =~ s/\s//g;
183         } elsif ($filter eq 'cuecat') {
184                 chomp($barcode);
185             my @fields = split( /\./, $barcode );
186             my @results = map( C4::Circulation::_decode($_), @fields[ 1 .. $#fields ] );
187             ($#results == 2) and return $results[2];
188         } elsif ($filter eq 'T-prefix') {
189                 if ($barcode =~ /^[Tt](\d)/) {
190                         (defined($1) and $1 eq '0') and return $barcode;
191             $barcode = substr($barcode, 2) + 0;     # FIXME: probably should be substr($barcode, 1)
192                 }
193         return sprintf("T%07d", $barcode);
194         # FIXME: $barcode could be "T1", causing warning: substr outside of string
195         # Why drop the nonzero digit after the T?
196         # Why pass non-digits (or empty string) to "T%07d"?
197         } elsif ($filter eq 'libsuite8') {
198                 unless($barcode =~ m/^($branch)-/i){    #if barcode starts with branch code its in Koha style. Skip it.
199                         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
200                                 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
201                         }else{
202                                 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
203                         }
204                 }
205     } elsif ($filter eq 'EAN13') {
206         my $ean = CheckDigits('ean');
207         if ( $ean->is_valid($barcode) ) {
208             #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
209             $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
210         } else {
211             warn "# [$barcode] not valid EAN-13/UPC-A\n";
212         }
213         }
214     return $barcode;    # return barcode, modified or not
215 }
216
217 =head2 _decode
218
219   $str = &_decode($chunk);
220
221 Decodes a segment of a string emitted by a CueCat barcode scanner and
222 returns it.
223
224 FIXME: Should be replaced with Barcode::Cuecat from CPAN
225 or Javascript based decoding on the client side.
226
227 =cut
228
229 sub _decode {
230     my ($encoded) = @_;
231     my $seq =
232       'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
233     my @s = map { index( $seq, $_ ); } split( //, $encoded );
234     my $l = ( $#s + 1 ) % 4;
235     if ($l) {
236         if ( $l == 1 ) {
237             # warn "Error: Cuecat decode parsing failed!";
238             return;
239         }
240         $l = 4 - $l;
241         $#s += $l;
242     }
243     my $r = '';
244     while ( $#s >= 0 ) {
245         my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
246         $r .=
247             chr( ( $n >> 16 ) ^ 67 )
248          .chr( ( $n >> 8 & 255 ) ^ 67 )
249          .chr( ( $n & 255 ) ^ 67 );
250         @s = @s[ 4 .. $#s ];
251     }
252     $r = substr( $r, 0, length($r) - $l );
253     return $r;
254 }
255
256 =head2 transferbook
257
258   ($dotransfer, $messages, $iteminformation) = &transferbook({
259                                                    from_branch => $frombranch
260                                                    to_branch => $tobranch,
261                                                    barcode => $barcode,
262                                                    ignore_reserves => $ignore_reserves,
263                                                    trigger => $trigger
264                                                 });
265
266 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
267
268 C<$fbr> is the code for the branch initiating the transfer.
269 C<$tbr> is the code for the branch to which the item should be transferred.
270
271 C<$barcode> is the barcode of the item to be transferred.
272
273 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
274 Otherwise, if an item is reserved, the transfer fails.
275
276 C<$trigger> is the enum value for what triggered the transfer.
277
278 Returns three values:
279
280 =over
281
282 =item $dotransfer 
283
284 is true if the transfer was successful.
285
286 =item $messages
287
288 is a reference-to-hash which may have any of the following keys:
289
290 =over
291
292 =item C<BadBarcode>
293
294 There is no item in the catalog with the given barcode. The value is C<$barcode>.
295
296 =item C<DestinationEqualsHolding>
297
298 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.
299
300 =item C<WasReturned>
301
302 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.
303
304 =item C<ResFound>
305
306 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>.
307
308 =item C<WasTransferred>
309
310 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
311
312 =back
313
314 =back
315
316 =cut
317
318 sub transferbook {
319     my $params = shift;
320     my $tbr      = $params->{to_branch};
321     my $fbr      = $params->{from_branch};
322     my $ignoreRs = $params->{ignore_reserves};
323     my $barcode  = $params->{barcode};
324     my $trigger  = $params->{trigger};
325     my $messages;
326     my $dotransfer      = 1;
327     my $item = Koha::Items->find( { barcode => $barcode } );
328
329     Koha::Exceptions::MissingParameter->throw(
330         "Missing mandatory parameter: from_branch")
331       unless $fbr;
332
333     Koha::Exceptions::MissingParameter->throw(
334         "Missing mandatory parameter: to_branch")
335       unless $tbr;
336
337     # bad barcode..
338     unless ( $item ) {
339         $messages->{'BadBarcode'} = $barcode;
340         $dotransfer = 0;
341         return ( $dotransfer, $messages );
342     }
343
344     my $itemnumber = $item->itemnumber;
345     # get branches of book...
346     my $hbr = $item->homebranch;
347
348     # if using Branch Transfer Limits
349     if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
350         my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
351         if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
352             if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
353                 $messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
354                 $dotransfer = 0;
355             }
356         } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
357             $messages->{'NotAllowed'} = $tbr . "::" . $code;
358             $dotransfer = 0;
359         }
360     }
361
362     # can't transfer book if is already there....
363     if ( $fbr eq $tbr ) {
364         $messages->{'DestinationEqualsHolding'} = 1;
365         $dotransfer = 0;
366     }
367
368     # check if it is still issued to someone, return it...
369     my $issue = Koha::Checkouts->find({ itemnumber => $itemnumber });
370     if ( $issue ) {
371         AddReturn( $barcode, $fbr );
372         $messages->{'WasReturned'} = $issue->borrowernumber;
373     }
374
375     # find reserves.....
376     # That'll save a database query.
377     my ( $resfound, $resrec, undef ) =
378       CheckReserves( $itemnumber );
379     if ( $resfound and not $ignoreRs ) {
380         $resrec->{'ResFound'} = $resfound;
381         $messages->{'ResFound'} = $resrec;
382         $dotransfer = 1;
383     }
384
385     #actually do the transfer....
386     if ($dotransfer) {
387         ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
388
389         # don't need to update MARC anymore, we do it in batch now
390         $messages->{'WasTransfered'} = 1;
391
392     }
393     ModDateLastSeen( $itemnumber );
394     return ( $dotransfer, $messages );
395 }
396
397
398 sub TooMany {
399     my $borrower        = shift;
400     my $item_object = shift;
401     my $params = shift;
402     my $onsite_checkout = $params->{onsite_checkout} || 0;
403     my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
404     my $cat_borrower    = $borrower->{'categorycode'};
405     my $dbh             = C4::Context->dbh;
406     # Get which branchcode we need
407     my $branch = _GetCircControlBranch($item_object->unblessed,$borrower);
408     my $type = $item_object->effective_itemtype;
409
410     my ($type_object, $parent_type, $parent_maxissueqty_rule);
411     $type_object = Koha::ItemTypes->find( $type );
412     $parent_type = $type_object->parent_type if $type_object;
413     my $child_types = Koha::ItemTypes->search({ parent_type => $type });
414     # Find any children if we are a parent_type;
415
416     # given branch, patron category, and item type, determine
417     # applicable issuing rule
418
419     $parent_maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
420         {
421             categorycode => $cat_borrower,
422             itemtype     => $parent_type,
423             branchcode   => $branch,
424             rule_name    => 'maxissueqty',
425         }
426     ) if $parent_type;
427     # If the parent rule is for default type we discount it
428     $parent_maxissueqty_rule = undef if $parent_maxissueqty_rule && !defined $parent_maxissueqty_rule->itemtype;
429
430     my $maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
431         {
432             categorycode => $cat_borrower,
433             itemtype     => $type,
434             branchcode   => $branch,
435             rule_name    => 'maxissueqty',
436         }
437     );
438
439     my $maxonsiteissueqty_rule = Koha::CirculationRules->get_effective_rule(
440         {
441             categorycode => $cat_borrower,
442             itemtype     => $type,
443             branchcode   => $branch,
444             rule_name    => 'maxonsiteissueqty',
445         }
446     );
447
448
449     my $patron = Koha::Patrons->find($borrower->{borrowernumber});
450     # if a rule is found and has a loan limit set, count
451     # how many loans the patron already has that meet that
452     # rule
453     if (defined($maxissueqty_rule) and $maxissueqty_rule->rule_value ne "") {
454
455         my $checkouts;
456         if ( $maxissueqty_rule->branchcode ) {
457             if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
458                 $checkouts = $patron->checkouts->search(
459                     { 'me.branchcode' => $maxissueqty_rule->branchcode } );
460             } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
461                 $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
462             } else {
463                 $checkouts = $patron->checkouts->search(
464                     { 'item.homebranch' => $maxissueqty_rule->branchcode },
465                     { prefetch          => 'item' } );
466             }
467         } else {
468             $checkouts = $patron->checkouts; # if rule is not branch specific then count all loans by patron
469         }
470         my $sum_checkouts;
471         my $rule_itemtype = $maxissueqty_rule->itemtype;
472         while ( my $c = $checkouts->next ) {
473             my $itemtype = $c->item->effective_itemtype;
474             my @types;
475             unless ( $rule_itemtype ) {
476                 # matching rule has the default item type, so count only
477                 # those existing loans that don't fall under a more
478                 # specific rule
479                 @types = Koha::CirculationRules->search(
480                     {
481                         branchcode => $maxissueqty_rule->branchcode,
482                         categorycode => [ $maxissueqty_rule->categorycode, $cat_borrower ],
483                         itemtype  => { '!=' => undef },
484                         rule_name => 'maxissueqty'
485                     }
486                 )->get_column('itemtype');
487
488                 next if grep {$_ eq $itemtype} @types;
489             } else {
490                 my @types;
491                 if ( $parent_maxissueqty_rule ) {
492                 # if we have a parent item type then we count loans of the
493                 # specific item type or its siblings or parent
494                     my $children = Koha::ItemTypes->search({ parent_type => $parent_type });
495                     @types = $children->get_column('itemtype');
496                     push @types, $parent_type;
497                 } elsif ( $child_types ) {
498                 # If we are a parent type, we need to count all child types and our own type
499                     @types = $child_types->get_column('itemtype');
500                     push @types, $type; # And don't forget to count our own types
501                 } else { push @types, $type; } # Otherwise only count the specific itemtype
502
503                 next unless grep {$_ eq $itemtype} @types;
504             }
505             $sum_checkouts->{total}++;
506             $sum_checkouts->{onsite_checkouts}++ if $c->onsite_checkout;
507             $sum_checkouts->{itemtype}->{$itemtype}++;
508         }
509
510         my $checkout_count_type = $sum_checkouts->{itemtype}->{$type} || 0;
511         my $checkout_count = $sum_checkouts->{total} || 0;
512         my $onsite_checkout_count = $sum_checkouts->{onsite_checkouts} || 0;
513
514         my $checkout_rules = {
515             checkout_count               => $checkout_count,
516             onsite_checkout_count        => $onsite_checkout_count,
517             onsite_checkout              => $onsite_checkout,
518             max_checkouts_allowed        => $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef,
519             max_onsite_checkouts_allowed => $maxonsiteissueqty_rule ? $maxonsiteissueqty_rule->rule_value : undef,
520             switch_onsite_checkout       => $switch_onsite_checkout,
521         };
522         # If parent rules exists
523         if ( defined($parent_maxissueqty_rule) and defined($parent_maxissueqty_rule->rule_value) ){
524             $checkout_rules->{max_checkouts_allowed} = $parent_maxissueqty_rule ? $parent_maxissueqty_rule->rule_value : undef;
525             my $qty_over = _check_max_qty($checkout_rules);
526             return $qty_over if defined $qty_over;
527
528             # If the parent rule is less than or equal to the child, we only need check the parent
529             if( $maxissueqty_rule->rule_value < $parent_maxissueqty_rule->rule_value && defined($maxissueqty_rule->itemtype) ) {
530                 $checkout_rules->{checkout_count} = $checkout_count_type;
531                 $checkout_rules->{max_checkouts_allowed} = $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef;
532                 my $qty_over = _check_max_qty($checkout_rules);
533                 return $qty_over if defined $qty_over;
534             }
535         } else {
536             my $qty_over = _check_max_qty($checkout_rules);
537             return $qty_over if defined $qty_over;
538         }
539     }
540
541     # Now count total loans against the limit for the branch
542     my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
543     if (defined($branch_borrower_circ_rule->{patron_maxissueqty}) and $branch_borrower_circ_rule->{patron_maxissueqty} ne '') {
544         my $checkouts;
545         if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
546             $checkouts = $patron->checkouts->search(
547                 { 'me.branchcode' => $branch} );
548         } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
549             $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
550         } else {
551             $checkouts = $patron->checkouts->search(
552                 { 'item.homebranch' => $branch},
553                 { prefetch          => 'item' } );
554         }
555
556         my $checkout_count = $checkouts->count;
557         my $onsite_checkout_count = $checkouts->search({ onsite_checkout => 1 })->count;
558         my $max_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxissueqty};
559         my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxonsiteissueqty} || undef;
560
561         my $qty_over = _check_max_qty(
562             {
563                 checkout_count               => $checkout_count,
564                 onsite_checkout_count        => $onsite_checkout_count,
565                 onsite_checkout              => $onsite_checkout,
566                 max_checkouts_allowed        => $max_checkouts_allowed,
567                 max_onsite_checkouts_allowed => $max_onsite_checkouts_allowed,
568                 switch_onsite_checkout       => $switch_onsite_checkout
569             }
570         );
571         return $qty_over if defined $qty_over;
572     }
573
574     if ( not defined( $maxissueqty_rule ) and not defined($branch_borrower_circ_rule->{patron_maxissueqty}) ) {
575         return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
576     }
577
578     # OK, the patron can issue !!!
579     return;
580 }
581
582 sub _check_max_qty {
583     my $params                       = shift;
584     my $checkout_count               = $params->{checkout_count};
585     my $onsite_checkout_count        = $params->{onsite_checkout_count};
586     my $onsite_checkout              = $params->{onsite_checkout};
587     my $max_checkouts_allowed        = $params->{max_checkouts_allowed};
588     my $max_onsite_checkouts_allowed = $params->{max_onsite_checkouts_allowed};
589     my $switch_onsite_checkout       = $params->{switch_onsite_checkout};
590
591     if ( $onsite_checkout and defined $max_onsite_checkouts_allowed ) {
592         if ( $max_onsite_checkouts_allowed eq '' ) { return; }
593         if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
594             return {
595                 reason      => 'TOO_MANY_ONSITE_CHECKOUTS',
596                 count       => $onsite_checkout_count,
597                 max_allowed => $max_onsite_checkouts_allowed,
598             };
599         }
600     }
601     if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
602         if ( $max_checkouts_allowed eq '' ) { return; }
603         my $delta = $switch_onsite_checkout ? 1 : 0;
604         if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
605             return {
606                 reason      => 'TOO_MANY_CHECKOUTS',
607                 count       => $checkout_count,
608                 max_allowed => $max_checkouts_allowed,
609             };
610         }
611     }
612     elsif ( not $onsite_checkout ) {
613         if ( $max_checkouts_allowed eq '' ) { return; }
614         if (
615             $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )
616         {
617             return {
618                 reason      => 'TOO_MANY_CHECKOUTS',
619                 count       => $checkout_count - $onsite_checkout_count,
620                 max_allowed => $max_checkouts_allowed,
621             };
622         }
623     }
624
625     return;
626 }
627
628 =head2 CanBookBeIssued
629
630   ( $issuingimpossible, $needsconfirmation, [ $alerts ] ) =  CanBookBeIssued( $patron,
631                       $barcode, $duedate, $inprocess, $ignore_reserves, $params );
632
633 Check if a book can be issued.
634
635 C<$issuingimpossible> and C<$needsconfirmation> are hashrefs.
636
637 IMPORTANT: The assumption by users of this routine is that causes blocking
638 the issue are keyed by uppercase labels and other returned
639 data is keyed in lower case!
640
641 =over 4
642
643 =item C<$patron> is a Koha::Patron
644
645 =item C<$barcode> is the bar code of the book being issued.
646
647 =item C<$duedates> is a DateTime object.
648
649 =item C<$inprocess> boolean switch
650
651 =item C<$ignore_reserves> boolean switch
652
653 =item C<$params> Hashref of additional parameters
654
655 Available keys:
656     override_high_holds - Ignore high holds
657     onsite_checkout     - Checkout is an onsite checkout that will not leave the library
658
659 =back
660
661 Returns :
662
663 =over 4
664
665 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
666 Possible values are :
667
668 =back
669
670 =head3 INVALID_DATE 
671
672 sticky due date is invalid
673
674 =head3 GNA
675
676 borrower gone with no address
677
678 =head3 CARD_LOST
679
680 borrower declared it's card lost
681
682 =head3 DEBARRED
683
684 borrower debarred
685
686 =head3 UNKNOWN_BARCODE
687
688 barcode unknown
689
690 =head3 NOT_FOR_LOAN
691
692 item is not for loan
693
694 =head3 WTHDRAWN
695
696 item withdrawn.
697
698 =head3 RESTRICTED
699
700 item is restricted (set by ??)
701
702 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan 
703 could be prevented, but ones that can be overriden by the operator.
704
705 Possible values are :
706
707 =head3 DEBT
708
709 borrower has debts.
710
711 =head3 RENEW_ISSUE
712
713 renewing, not issuing
714
715 =head3 ISSUED_TO_ANOTHER
716
717 issued to someone else.
718
719 =head3 RESERVED
720
721 reserved for someone else.
722
723 =head3 INVALID_DATE
724
725 sticky due date is invalid or due date in the past
726
727 =head3 TOO_MANY
728
729 if the borrower borrows to much things
730
731 =cut
732
733 sub CanBookBeIssued {
734     my ( $patron, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
735     my %needsconfirmation;    # filled with problems that needs confirmations
736     my %issuingimpossible;    # filled with problems that causes the issue to be IMPOSSIBLE
737     my %alerts;               # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
738     my %messages;             # filled with information messages that should be displayed.
739
740     my $onsite_checkout     = $params->{onsite_checkout}     || 0;
741     my $override_high_holds = $params->{override_high_holds} || 0;
742
743     my $item_object = Koha::Items->find({barcode => $barcode });
744
745     # MANDATORY CHECKS - unless item exists, nothing else matters
746     unless ( $item_object ) {
747         $issuingimpossible{UNKNOWN_BARCODE} = 1;
748     }
749     return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
750
751     my $item_unblessed = $item_object->unblessed; # Transition...
752     my $issue = $item_object->checkout;
753     my $biblio = $item_object->biblio;
754
755     my $biblioitem = $biblio->biblioitem;
756     my $effective_itemtype = $item_object->effective_itemtype;
757     my $dbh             = C4::Context->dbh;
758     my $patron_unblessed = $patron->unblessed;
759
760     my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
761     #
762     # DUE DATE is OK ? -- should already have checked.
763     #
764     if ($duedate && ref $duedate ne 'DateTime') {
765         $duedate = dt_from_string($duedate);
766     }
767     my $now = dt_from_string();
768     unless ( $duedate ) {
769         my $issuedate = $now->clone();
770
771         $duedate = CalcDateDue( $issuedate, $effective_itemtype, $circ_library->branchcode, $patron_unblessed );
772
773         # Offline circ calls AddIssue directly, doesn't run through here
774         #  So issuingimpossible should be ok.
775     }
776
777     my $fees = Koha::Charges::Fees->new(
778         {
779             patron    => $patron,
780             library   => $circ_library,
781             item      => $item_object,
782             to_date   => $duedate,
783         }
784     );
785
786     if ($duedate) {
787         my $today = $now->clone();
788         $today->truncate( to => 'minute');
789         if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
790             $needsconfirmation{INVALID_DATE} = output_pref($duedate);
791         }
792     } else {
793             $issuingimpossible{INVALID_DATE} = output_pref($duedate);
794     }
795
796     #
797     # BORROWER STATUS
798     #
799     if ( $patron->category->category_type eq 'X' && (  $item_object->barcode  )) {
800         # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1  .
801         &UpdateStats({
802                      branch => C4::Context->userenv->{'branch'},
803                      type => 'localuse',
804                      itemnumber => $item_object->itemnumber,
805                      itemtype => $effective_itemtype,
806                      borrowernumber => $patron->borrowernumber,
807                      ccode => $item_object->ccode}
808                     );
809         ModDateLastSeen( $item_object->itemnumber ); # FIXME Move to Koha::Item
810         return( { STATS => 1 }, {});
811     }
812
813     if ( $patron->gonenoaddress && $patron->gonenoaddress == 1 ) {
814         $issuingimpossible{GNA} = 1;
815     }
816
817     if ( $patron->lost && $patron->lost == 1 ) {
818         $issuingimpossible{CARD_LOST} = 1;
819     }
820     if ( $patron->is_debarred ) {
821         $issuingimpossible{DEBARRED} = 1;
822     }
823
824     if ( $patron->is_expired ) {
825         $issuingimpossible{EXPIRED} = 1;
826     }
827
828     #
829     # BORROWER STATUS
830     #
831
832     # DEBTS
833     my $account = $patron->account;
834     my $balance = $account->balance;
835     my $non_issues_charges = $account->non_issues_charges;
836     my $other_charges = $balance - $non_issues_charges;
837
838     my $amountlimit = C4::Context->preference("noissuescharge");
839     my $allowfineoverride = C4::Context->preference("AllowFineOverride");
840     my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
841
842     # Check the debt of this patrons guarantees
843     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
844     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
845     if ( defined $no_issues_charge_guarantees ) {
846         my @guarantees = map { $_->guarantee } $patron->guarantee_relationships();
847         my $guarantees_non_issues_charges = 0;
848         foreach my $g ( @guarantees ) {
849             $guarantees_non_issues_charges += $g->account->non_issues_charges;
850         }
851
852         if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
853             $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
854         } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
855             $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
856         } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
857             $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
858         }
859     }
860
861     # Check the debt of this patrons guarantors *and* the guarantees of those guarantors
862     my $no_issues_charge_guarantors = C4::Context->preference("NoIssuesChargeGuarantorsWithGuarantees");
863     $no_issues_charge_guarantors = undef unless looks_like_number( $no_issues_charge_guarantors );
864     if ( defined $no_issues_charge_guarantors ) {
865         my $guarantors_non_issues_charges += $patron->relationships_debt({ include_guarantors => 1, only_this_guarantor => 0, include_this_patron => 1 });
866
867         if ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && !$allowfineoverride) {
868             $issuingimpossible{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
869         } elsif ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && $allowfineoverride) {
870             $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
871         } elsif ( $allfinesneedoverride && $guarantors_non_issues_charges > 0 && $guarantors_non_issues_charges <= $no_issues_charge_guarantors && !$inprocess ) {
872             $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
873         }
874     }
875
876     if ( C4::Context->preference("IssuingInProcess") ) {
877         if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
878             $issuingimpossible{DEBT} = $non_issues_charges;
879         } elsif ( $non_issues_charges > $amountlimit && !$inprocess && $allowfineoverride) {
880             $needsconfirmation{DEBT} = $non_issues_charges;
881         } elsif ( $allfinesneedoverride && $non_issues_charges > 0 && $non_issues_charges <= $amountlimit && !$inprocess ) {
882             $needsconfirmation{DEBT} = $non_issues_charges;
883         }
884     }
885     else {
886         if ( $non_issues_charges > $amountlimit && $allowfineoverride ) {
887             $needsconfirmation{DEBT} = $non_issues_charges;
888         } elsif ( $non_issues_charges > $amountlimit && !$allowfineoverride) {
889             $issuingimpossible{DEBT} = $non_issues_charges;
890         } elsif ( $non_issues_charges > 0 && $allfinesneedoverride ) {
891             $needsconfirmation{DEBT} = $non_issues_charges;
892         }
893     }
894
895     if ($balance > 0 && $other_charges > 0) {
896         $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
897     }
898
899     $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
900     $patron_unblessed = $patron->unblessed;
901
902     if ( my $debarred_date = $patron->is_debarred ) {
903          # patron has accrued fine days or has a restriction. $count is a date
904         if ($debarred_date eq '9999-12-31') {
905             $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
906         }
907         else {
908             $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
909         }
910     } elsif ( my $num_overdues = $patron->has_overdues ) {
911         ## patron has outstanding overdue loans
912         if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
913             $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
914         }
915         elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
916             $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
917         }
918     }
919
920     # Additional Materials Check
921     if ( C4::Context->preference("CircConfirmItemParts")
922         && $item_object->materials )
923     {
924         $needsconfirmation{ADDITIONAL_MATERIALS} = $item_object->materials;
925     }
926
927     #
928     # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
929     #
930     if ( $issue && $issue->borrowernumber eq $patron->borrowernumber ){
931
932         # Already issued to current borrower.
933         # If it is an on-site checkout if it can be switched to a normal checkout
934         # or ask whether the loan should be renewed
935
936         if ( $issue->onsite_checkout
937                 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
938             $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
939         } else {
940             my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
941                 $patron->borrowernumber,
942                 $item_object->itemnumber,
943             );
944             if ( $CanBookBeRenewed == 0 ) {    # no more renewals allowed
945                 if ( $renewerror eq 'onsite_checkout' ) {
946                     $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
947                 }
948                 else {
949                     $issuingimpossible{NO_MORE_RENEWALS} = 1;
950                 }
951             }
952             else {
953                 $needsconfirmation{RENEW_ISSUE} = 1;
954             }
955         }
956     }
957     elsif ( $issue ) {
958
959         # issued to someone else
960
961         my $patron = Koha::Patrons->find( $issue->borrowernumber );
962
963         my ( $can_be_returned, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
964
965         unless ( $can_be_returned ) {
966             $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
967             $issuingimpossible{branch_to_return} = $message;
968         } else {
969             if ( C4::Context->preference('AutoReturnCheckedOutItems') ) {
970                 $alerts{RETURNED_FROM_ANOTHER} = { patron => $patron };
971             } else {
972             $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
973             $needsconfirmation{issued_firstname} = $patron->firstname;
974             $needsconfirmation{issued_surname} = $patron->surname;
975             $needsconfirmation{issued_cardnumber} = $patron->cardnumber;
976             $needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
977             }
978         }
979     }
980
981     # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
982     #
983     my $switch_onsite_checkout = (
984           C4::Context->preference('SwitchOnSiteCheckouts')
985       and $issue
986       and $issue->onsite_checkout
987       and $issue->borrowernumber == $patron->borrowernumber ? 1 : 0 );
988     my $toomany = TooMany( $patron_unblessed, $item_object, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
989     # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
990     if ( $toomany && not exists $needsconfirmation{RENEW_ISSUE} ) {
991         if ( $toomany->{max_allowed} == 0 ) {
992             $needsconfirmation{PATRON_CANT} = 1;
993         }
994         if ( C4::Context->preference("AllowTooManyOverride") ) {
995             $needsconfirmation{TOO_MANY} = $toomany->{reason};
996             $needsconfirmation{current_loan_count} = $toomany->{count};
997             $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
998         } else {
999             $issuingimpossible{TOO_MANY} = $toomany->{reason};
1000             $issuingimpossible{current_loan_count} = $toomany->{count};
1001             $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
1002         }
1003     }
1004
1005     #
1006     # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
1007     #
1008     $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
1009     my $wants_check = $patron->wants_check_for_previous_checkout;
1010     $needsconfirmation{PREVISSUE} = 1
1011         if ($wants_check and $patron->do_check_for_previous_checkout($item_unblessed));
1012
1013     #
1014     # ITEM CHECKING
1015     #
1016     if ( $item_object->notforloan )
1017     {
1018         if(!C4::Context->preference("AllowNotForLoanOverride")){
1019             $issuingimpossible{NOT_FOR_LOAN} = 1;
1020             $issuingimpossible{item_notforloan} = $item_object->notforloan;
1021         }else{
1022             $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1023             $needsconfirmation{item_notforloan} = $item_object->notforloan;
1024         }
1025     }
1026     else {
1027         # we have to check itemtypes.notforloan also
1028         if (C4::Context->preference('item-level_itypes')){
1029             # this should probably be a subroutine
1030             my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
1031             $sth->execute($effective_itemtype);
1032             my $notforloan=$sth->fetchrow_hashref();
1033             if ($notforloan->{'notforloan'}) {
1034                 if (!C4::Context->preference("AllowNotForLoanOverride")) {
1035                     $issuingimpossible{NOT_FOR_LOAN} = 1;
1036                     $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
1037                 } else {
1038                     $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1039                     $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
1040                 }
1041             }
1042         }
1043         else {
1044             my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype);
1045             if ( $itemtype && defined $itemtype->notforloan && $itemtype->notforloan == 1){
1046                 if (!C4::Context->preference("AllowNotForLoanOverride")) {
1047                     $issuingimpossible{NOT_FOR_LOAN} = 1;
1048                     $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
1049                 } else {
1050                     $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1051                     $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
1052                 }
1053             }
1054         }
1055     }
1056     if ( $item_object->withdrawn && $item_object->withdrawn > 0 )
1057     {
1058         $issuingimpossible{WTHDRAWN} = 1;
1059     }
1060     if (   $item_object->restricted
1061         && $item_object->restricted == 1 )
1062     {
1063         $issuingimpossible{RESTRICTED} = 1;
1064     }
1065     if ( $item_object->itemlost && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
1066         my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item_object->itemlost });
1067         my $code = $av->count ? $av->next->lib : '';
1068         $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
1069         $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
1070     }
1071     if ( C4::Context->preference("IndependentBranches") ) {
1072         my $userenv = C4::Context->userenv;
1073         unless ( C4::Context->IsSuperLibrarian() ) {
1074             my $HomeOrHoldingBranch = C4::Context->preference("HomeOrHoldingBranch");
1075             if ( $item_object->$HomeOrHoldingBranch ne $userenv->{branch} ){
1076                 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
1077                 $issuingimpossible{'itemhomebranch'} = $item_object->$HomeOrHoldingBranch;
1078             }
1079             $needsconfirmation{BORRNOTSAMEBRANCH} = $patron->branchcode
1080               if ( $patron->branchcode ne $userenv->{branch} );
1081         }
1082     }
1083
1084     #
1085     # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
1086     #
1087     my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
1088     if ($rentalConfirmation) {
1089         my ($rentalCharge) = GetIssuingCharges( $item_object->itemnumber, $patron->borrowernumber );
1090
1091         my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1092         if ($itemtype_object) {
1093             my $accumulate_charge = $fees->accumulate_rentalcharge();
1094             if ( $accumulate_charge > 0 ) {
1095                 $rentalCharge += $accumulate_charge;
1096             }
1097         }
1098
1099         if ( $rentalCharge > 0 ) {
1100             $needsconfirmation{RENTALCHARGE} = $rentalCharge;
1101         }
1102     }
1103
1104     unless ( $ignore_reserves ) {
1105         # See if the item is on reserve.
1106         my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
1107         if ($restype) {
1108             my $resbor = $res->{'borrowernumber'};
1109             if ( $resbor ne $patron->borrowernumber ) {
1110                 my $patron = Koha::Patrons->find( $resbor );
1111                 if ( $restype eq "Waiting" )
1112                 {
1113                     # The item is on reserve and waiting, but has been
1114                     # reserved by some other patron.
1115                     $needsconfirmation{RESERVE_WAITING} = 1;
1116                     $needsconfirmation{'resfirstname'} = $patron->firstname;
1117                     $needsconfirmation{'ressurname'} = $patron->surname;
1118                     $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1119                     $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1120                     $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1121                     $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1122                     $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1123                 }
1124                 elsif ( $restype eq "Reserved" ) {
1125                     # The item is on reserve for someone else.
1126                     $needsconfirmation{RESERVED} = 1;
1127                     $needsconfirmation{'resfirstname'} = $patron->firstname;
1128                     $needsconfirmation{'ressurname'} = $patron->surname;
1129                     $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1130                     $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1131                     $needsconfirmation{'resbranchcode'} = $patron->branchcode;
1132                     $needsconfirmation{'resreservedate'} = $res->{reservedate};
1133                     $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1134                 }
1135             }
1136         }
1137     }
1138
1139     ## CHECK AGE RESTRICTION
1140     my $agerestriction  = $biblioitem->agerestriction;
1141     my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );
1142     if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1143         if ( C4::Context->preference('AgeRestrictionOverride') ) {
1144             $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1145         }
1146         else {
1147             $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1148         }
1149     }
1150
1151     ## check for high holds decreasing loan period
1152     if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1153         my $check = checkHighHolds( $item_unblessed, $patron_unblessed );
1154
1155         if ( $check->{exceeded} ) {
1156             if ($override_high_holds) {
1157                 $alerts{HIGHHOLDS} = {
1158                     num_holds  => $check->{outstanding},
1159                     duration   => $check->{duration},
1160                     returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1161                 };
1162             }
1163             else {
1164                 $needsconfirmation{HIGHHOLDS} = {
1165                     num_holds  => $check->{outstanding},
1166                     duration   => $check->{duration},
1167                     returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1168                 };
1169             }
1170         }
1171     }
1172
1173     if (
1174         !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1175         # don't do the multiple loans per bib check if we've
1176         # already determined that we've got a loan on the same item
1177         !$issuingimpossible{NO_MORE_RENEWALS} &&
1178         !$needsconfirmation{RENEW_ISSUE}
1179     ) {
1180         # Check if borrower has already issued an item from the same biblio
1181         # Only if it's not a subscription
1182         my $biblionumber = $item_object->biblionumber;
1183         require C4::Serials;
1184         my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1185         unless ($is_a_subscription) {
1186             # FIXME Should be $patron->checkouts($args);
1187             my $checkouts = Koha::Checkouts->search(
1188                 {
1189                     borrowernumber => $patron->borrowernumber,
1190                     biblionumber   => $biblionumber,
1191                 },
1192                 {
1193                     join => 'item',
1194                 }
1195             );
1196             # if we get here, we don't already have a loan on this item,
1197             # so if there are any loans on this bib, ask for confirmation
1198             if ( $checkouts->count ) {
1199                 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1200             }
1201         }
1202     }
1203
1204     return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1205 }
1206
1207 =head2 CanBookBeReturned
1208
1209   ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1210
1211 Check whether the item can be returned to the provided branch
1212
1213 =over 4
1214
1215 =item C<$item> is a hash of item information as returned Koha::Items->find->unblessed (Temporary, should be a Koha::Item instead)
1216
1217 =item C<$branch> is the branchcode where the return is taking place
1218
1219 =back
1220
1221 Returns:
1222
1223 =over 4
1224
1225 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1226
1227 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1228
1229 =back
1230
1231 =cut
1232
1233 sub CanBookBeReturned {
1234   my ($item, $branch) = @_;
1235   my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1236
1237   # assume return is allowed to start
1238   my $allowed = 1;
1239   my $message;
1240
1241   # identify all cases where return is forbidden
1242   if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1243      $allowed = 0;
1244      $message = $item->{'homebranch'};
1245   } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1246      $allowed = 0;
1247      $message = $item->{'holdingbranch'};
1248   } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1249      $allowed = 0;
1250      $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1251   }
1252
1253   return ($allowed, $message);
1254 }
1255
1256 =head2 CheckHighHolds
1257
1258     used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1259     decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1260     has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1261
1262 =cut
1263
1264 sub checkHighHolds {
1265     my ( $item, $borrower ) = @_;
1266     my $branchcode = _GetCircControlBranch( $item, $borrower );
1267     my $item_object = Koha::Items->find( $item->{itemnumber} );
1268
1269     my $return_data = {
1270         exceeded    => 0,
1271         outstanding => 0,
1272         duration    => 0,
1273         due_date    => undef,
1274     };
1275
1276     my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1277
1278     if ( $holds->count() ) {
1279         $return_data->{outstanding} = $holds->count();
1280
1281         my $decreaseLoanHighHoldsControl        = C4::Context->preference('decreaseLoanHighHoldsControl');
1282         my $decreaseLoanHighHoldsValue          = C4::Context->preference('decreaseLoanHighHoldsValue');
1283         my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1284
1285         my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1286
1287         if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1288
1289             # static means just more than a given number of holds on the record
1290
1291             # If the number of holds is less than the threshold, we can stop here
1292             if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1293                 return $return_data;
1294             }
1295         }
1296         elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1297
1298             # dynamic means X more than the number of holdable items on the record
1299
1300             # let's get the items
1301             my @items = $holds->next()->biblio()->items()->as_list;
1302
1303             # Remove any items with status defined to be ignored even if the would not make item unholdable
1304             foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1305                 @items = grep { !$_->$status } @items;
1306             }
1307
1308             # Remove any items that are not holdable for this patron
1309             @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items;
1310
1311             my $items_count = scalar @items;
1312
1313             my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1314
1315             # If the number of holds is less than the count of items we have
1316             # plus the number of holds allowed above that count, we can stop here
1317             if ( $holds->count() <= $threshold ) {
1318                 return $return_data;
1319             }
1320         }
1321
1322         my $issuedate = dt_from_string();
1323
1324         my $itype = $item_object->effective_itemtype;
1325         my $daysmode = Koha::CirculationRules->get_effective_daysmode(
1326             {
1327                 categorycode => $borrower->{categorycode},
1328                 itemtype     => $itype,
1329                 branchcode   => $branchcode,
1330             }
1331         );
1332         my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
1333
1334         my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1335
1336         my $rule = Koha::CirculationRules->get_effective_rule(
1337             {
1338                 categorycode => $borrower->{categorycode},
1339                 itemtype     => $item_object->effective_itemtype,
1340                 branchcode   => $branchcode,
1341                 rule_name    => 'decreaseloanholds',
1342             }
1343         );
1344
1345         my $duration;
1346         if ( defined($rule) && $rule->rule_value ne '' ){
1347             # overrides decreaseLoanHighHoldsDuration syspref
1348             $duration = $rule->rule_value;
1349         } else {
1350             $duration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1351         }
1352         my $reduced_datedue = $calendar->addDuration( $issuedate, $duration );
1353         $reduced_datedue->set_hour($orig_due->hour);
1354         $reduced_datedue->set_minute($orig_due->minute);
1355         $reduced_datedue->truncate( to => 'minute' );
1356
1357         if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1358             $return_data->{exceeded} = 1;
1359             $return_data->{duration} = $duration;
1360             $return_data->{due_date} = $reduced_datedue;
1361         }
1362     }
1363
1364     return $return_data;
1365 }
1366
1367 =head2 AddIssue
1368
1369   &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1370
1371 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1372
1373 =over 4
1374
1375 =item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
1376
1377 =item C<$barcode> is the barcode of the item being issued.
1378
1379 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1380 Calculated if empty.
1381
1382 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1383
1384 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1385 Defaults to today.  Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1386
1387 AddIssue does the following things :
1388
1389   - step 01: check that there is a borrowernumber & a barcode provided
1390   - check for RENEWAL (book issued & being issued to the same patron)
1391       - renewal YES = Calculate Charge & renew
1392       - renewal NO  =
1393           * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1394           * RESERVE PLACED ?
1395               - fill reserve if reserve to this patron
1396               - cancel reserve or not, otherwise
1397           * TRANSFERT PENDING ?
1398               - complete the transfert
1399           * ISSUE THE BOOK
1400
1401 =back
1402
1403 =cut
1404
1405 sub AddIssue {
1406     my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1407
1408     my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1409     my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1410     my $auto_renew = $params && $params->{auto_renew};
1411     my $dbh          = C4::Context->dbh;
1412     my $barcodecheck = CheckValidBarcode($barcode);
1413
1414     my $issue;
1415
1416     if ( $datedue && ref $datedue ne 'DateTime' ) {
1417         $datedue = dt_from_string($datedue);
1418     }
1419
1420     # $issuedate defaults to today.
1421     if ( !defined $issuedate ) {
1422         $issuedate = dt_from_string();
1423     }
1424     else {
1425         if ( ref $issuedate ne 'DateTime' ) {
1426             $issuedate = dt_from_string($issuedate);
1427
1428         }
1429     }
1430
1431     # Stop here if the patron or barcode doesn't exist
1432     if ( $borrower && $barcode && $barcodecheck ) {
1433         # find which item we issue
1434         my $item_object = Koha::Items->find({ barcode => $barcode })
1435           or return;    # if we don't get an Item, abort.
1436         my $item_unblessed = $item_object->unblessed;
1437
1438         my $branchcode = _GetCircControlBranch( $item_unblessed, $borrower );
1439
1440         # get actual issuing if there is one
1441         my $actualissue = $item_object->checkout;
1442
1443         # check if we just renew the issue.
1444         if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
1445                 and not $switch_onsite_checkout ) {
1446             $datedue = AddRenewal(
1447                 $borrower->{'borrowernumber'},
1448                 $item_object->itemnumber,
1449                 $branchcode,
1450                 $datedue,
1451                 $issuedate,    # here interpreted as the renewal date
1452             );
1453         }
1454         else {
1455             unless ($datedue) {
1456                 my $itype = $item_object->effective_itemtype;
1457                 $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1458
1459             }
1460             $datedue->truncate( to => 'minute' );
1461
1462             my $patron = Koha::Patrons->find( $borrower );
1463             my $library = Koha::Libraries->find( $branchcode );
1464             my $fees = Koha::Charges::Fees->new(
1465                 {
1466                     patron    => $patron,
1467                     library   => $library,
1468                     item      => $item_object,
1469                     to_date   => $datedue,
1470                 }
1471             );
1472
1473             # it's NOT a renewal
1474             if ( $actualissue and not $switch_onsite_checkout ) {
1475                 # This book is currently on loan, but not to the person
1476                 # who wants to borrow it now. mark it returned before issuing to the new borrower
1477                 my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
1478                 return unless $allowed;
1479                 AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
1480                 # AddReturn certainly has side-effects, like onloan => undef
1481                 $item_object->discard_changes;
1482             }
1483
1484             C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
1485
1486             # Starting process for transfer job (checking transfert and validate it if we have one)
1487             if ( my $transfer = $item_object->get_transfer ) {
1488                 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1489                 $transfer->set(
1490                     {
1491                         datearrived => dt_from_string,
1492                         tobranch    => C4::Context->userenv->{branch},
1493                         comments    => 'Forced branchtransfer'
1494                     }
1495                 )->store;
1496                 if ( $transfer->reason && $transfer->reason eq 'Reserve' ) {
1497                     my $hold = $item_object->holds->search( { found => 'T' } )->next;
1498                     if ( $hold ) { # Is this really needed?
1499                         $hold->set( { found => undef } )->store;
1500                         C4::Reserves::ModReserveMinusPriority($item_object->itemnumber, $hold->reserve_id);
1501                     }
1502                 }
1503             }
1504
1505             # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1506             unless ($auto_renew) {
1507                 my $rule = Koha::CirculationRules->get_effective_rule(
1508                     {
1509                         categorycode => $borrower->{categorycode},
1510                         itemtype     => $item_object->effective_itemtype,
1511                         branchcode   => $branchcode,
1512                         rule_name    => 'auto_renew'
1513                     }
1514                 );
1515
1516                 $auto_renew = $rule->rule_value if $rule;
1517             }
1518
1519             my $issue_attributes = {
1520                 borrowernumber  => $borrower->{'borrowernumber'},
1521                 issuedate       => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1522                 date_due        => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1523                 branchcode      => C4::Context->userenv->{'branch'},
1524                 onsite_checkout => $onsite_checkout,
1525                 auto_renew      => $auto_renew ? 1 : 0,
1526             };
1527
1528             # Get ID of logged in user.  if called from a batch job,
1529             # no user session exists and C4::Context->userenv() returns
1530             # the scalar '0'. Only do this if the syspref says so
1531             if ( C4::Context->preference('RecordStaffUserOnCheckout') ) {
1532                 my $userenv = C4::Context->userenv();
1533                 my $usernumber = (ref($userenv) eq 'HASH') ? $userenv->{'number'} : undef;
1534                 if ($usernumber) {
1535                     $issue_attributes->{issuer_id} = $usernumber;
1536                 }
1537             }
1538
1539             # In the case that the borrower has an on-site checkout
1540             # and SwitchOnSiteCheckouts is enabled this converts it to a regular checkout
1541             $issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
1542             if ($issue) {
1543                 $issue->set($issue_attributes)->store;
1544             }
1545             else {
1546                 $issue = Koha::Checkout->new(
1547                     {
1548                         itemnumber => $item_object->itemnumber,
1549                         %$issue_attributes,
1550                     }
1551                 )->store;
1552             }
1553             $issue->discard_changes;
1554             if ( $item_object->location && $item_object->location eq 'CART'
1555                 && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
1556             ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
1557                 CartToShelf( $item_object->itemnumber );
1558             }
1559
1560             if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1561                 UpdateTotalIssues( $item_object->biblionumber, 1 );
1562             }
1563
1564             # Record if item was lost
1565             my $was_lost = $item_object->itemlost;
1566
1567             $item_object->issues( ( $item_object->issues || 0 ) + 1);
1568             $item_object->holdingbranch(C4::Context->userenv->{'branch'});
1569             $item_object->itemlost(0);
1570             $item_object->onloan($datedue->ymd());
1571             $item_object->datelastborrowed( dt_from_string()->ymd() );
1572             $item_object->datelastseen( dt_from_string()->ymd() );
1573             $item_object->store({log_action => 0});
1574
1575             # If the item was lost, it has now been found, charge the overdue if necessary
1576             if ($was_lost) {
1577                 if ( $item_object->{_charge} ) {
1578                     $actualissue //= Koha::Old::Checkouts->search(
1579                         { itemnumber => $item_unblessed->{itemnumber} },
1580                         {
1581                             order_by => { '-desc' => 'returndate' },
1582                             rows     => 1
1583                         }
1584                     )->single;
1585                     unless ( exists( $borrower->{branchcode} ) ) {
1586                         my $patron = $actualissue->patron;
1587                         $borrower = $patron->unblessed;
1588                     }
1589                     _CalculateAndUpdateFine(
1590                         {
1591                             issue       => $actualissue,
1592                             item        => $item_unblessed,
1593                             borrower    => $borrower,
1594                             return_date => $issuedate
1595                         }
1596                     );
1597                     _FixOverduesOnReturn( $borrower->{borrowernumber},
1598                         $item_object->itemnumber, undef, 'RENEWED' );
1599                 }
1600             }
1601
1602             # If it costs to borrow this book, charge it to the patron's account.
1603             my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
1604             if ( $charge && $charge > 0 ) {
1605                 AddIssuingCharge( $issue, $charge, 'RENT' );
1606             }
1607
1608             my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1609             if ( $itemtype_object ) {
1610                 my $accumulate_charge = $fees->accumulate_rentalcharge();
1611                 if ( $accumulate_charge > 0 ) {
1612                     AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' );
1613                     $charge += $accumulate_charge;
1614                     $item_unblessed->{charge} = $charge;
1615                 }
1616             }
1617
1618             # Record the fact that this book was issued.
1619             &UpdateStats(
1620                 {
1621                     branch => C4::Context->userenv->{'branch'},
1622                     type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1623                     amount         => $charge,
1624                     other          => ( $sipmode ? "SIP-$sipmode" : '' ),
1625                     itemnumber     => $item_object->itemnumber,
1626                     itemtype       => $item_object->effective_itemtype,
1627                     location       => $item_object->location,
1628                     borrowernumber => $borrower->{'borrowernumber'},
1629                     ccode          => $item_object->ccode,
1630                 }
1631             );
1632
1633             # Send a checkout slip.
1634             my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1635             my %conditions        = (
1636                 branchcode   => $branchcode,
1637                 categorycode => $borrower->{categorycode},
1638                 item_type    => $item_object->effective_itemtype,
1639                 notification => 'CHECKOUT',
1640             );
1641             if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1642                 SendCirculationAlert(
1643                     {
1644                         type     => 'CHECKOUT',
1645                         item     => $item_object->unblessed,
1646                         borrower => $borrower,
1647                         branch   => $branchcode,
1648                     }
1649                 );
1650             }
1651             logaction(
1652                 "CIRCULATION", "ISSUE",
1653                 $borrower->{'borrowernumber'},
1654                 $item_object->itemnumber,
1655             ) if C4::Context->preference("IssueLog");
1656
1657             Koha::Plugins->call('after_circ_action', {
1658                 action  => 'checkout',
1659                 payload => {
1660                     type     => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1661                     checkout => $issue->get_from_storage
1662                 }
1663             });
1664         }
1665     }
1666     return $issue;
1667 }
1668
1669 =head2 GetLoanLength
1670
1671   my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1672
1673 Get loan length for an itemtype, a borrower type and a branch
1674
1675 =cut
1676
1677 sub GetLoanLength {
1678     my ( $categorycode, $itemtype, $branchcode ) = @_;
1679
1680     # Initialize default values
1681     my $rules = {
1682         issuelength   => 0,
1683         renewalperiod => 0,
1684         lengthunit    => 'days',
1685     };
1686
1687     my $found = Koha::CirculationRules->get_effective_rules( {
1688         branchcode => $branchcode,
1689         categorycode => $categorycode,
1690         itemtype => $itemtype,
1691         rules => [
1692             'issuelength',
1693             'renewalperiod',
1694             'lengthunit'
1695         ],
1696     } );
1697
1698     # Search for rules!
1699     foreach my $rule_name (keys %$found) {
1700         $rules->{$rule_name} = $found->{$rule_name};
1701     }
1702
1703     return $rules;
1704 }
1705
1706
1707 =head2 GetHardDueDate
1708
1709   my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1710
1711 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1712
1713 =cut
1714
1715 sub GetHardDueDate {
1716     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1717
1718     my $rules = Koha::CirculationRules->get_effective_rules(
1719         {
1720             categorycode => $borrowertype,
1721             itemtype     => $itemtype,
1722             branchcode   => $branchcode,
1723             rules        => [ 'hardduedate', 'hardduedatecompare' ],
1724         }
1725     );
1726
1727     if ( defined( $rules->{hardduedate} ) ) {
1728         if ( $rules->{hardduedate} ) {
1729             return ( dt_from_string( $rules->{hardduedate}, 'iso' ), $rules->{hardduedatecompare} );
1730         }
1731         else {
1732             return ( undef, undef );
1733         }
1734     }
1735 }
1736
1737 =head2 GetBranchBorrowerCircRule
1738
1739   my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1740
1741 Retrieves circulation rule attributes that apply to the given
1742 branch and patron category, regardless of item type.  
1743 The return value is a hashref containing the following key:
1744
1745 patron_maxissueqty - maximum number of loans that a
1746 patron of the given category can have at the given
1747 branch.  If the value is undef, no limit.
1748
1749 patron_maxonsiteissueqty - maximum of on-site checkouts that a
1750 patron of the given category can have at the given
1751 branch.  If the value is undef, no limit.
1752
1753 This will check for different branch/category combinations in the following order:
1754 branch and category
1755 branch only
1756 category only
1757 default branch and category
1758
1759 If no rule has been found in the database, it will default to
1760 the buillt in rule:
1761
1762 patron_maxissueqty - undef
1763 patron_maxonsiteissueqty - undef
1764
1765 C<$branchcode> and C<$categorycode> should contain the
1766 literal branch code and patron category code, respectively - no
1767 wildcards.
1768
1769 =cut
1770
1771 sub GetBranchBorrowerCircRule {
1772     my ( $branchcode, $categorycode ) = @_;
1773
1774     # Initialize default values
1775     my $rules = {
1776         patron_maxissueqty       => undef,
1777         patron_maxonsiteissueqty => undef,
1778     };
1779
1780     # Search for rules!
1781     foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
1782         my $rule = Koha::CirculationRules->get_effective_rule(
1783             {
1784                 categorycode => $categorycode,
1785                 itemtype     => undef,
1786                 branchcode   => $branchcode,
1787                 rule_name    => $rule_name,
1788             }
1789         );
1790
1791         $rules->{$rule_name} = $rule->rule_value if defined $rule;
1792     }
1793
1794     return $rules;
1795 }
1796
1797 =head2 GetBranchItemRule
1798
1799   my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1800
1801 Retrieves circulation rule attributes that apply to the given
1802 branch and item type, regardless of patron category.
1803
1804 The return value is a hashref containing the following keys:
1805
1806 holdallowed => Hold policy for this branch and itemtype. Possible values:
1807   0: No holds allowed.
1808   1: Holds allowed only by patrons that have the same homebranch as the item.
1809   2: Holds allowed from any patron.
1810
1811 returnbranch => branch to which to return item.  Possible values:
1812   noreturn: do not return, let item remain where checked in (floating collections)
1813   homebranch: return to item's home branch
1814   holdingbranch: return to issuer branch
1815
1816 This searches branchitemrules in the following order:
1817
1818   * Same branchcode and itemtype
1819   * Same branchcode, itemtype '*'
1820   * branchcode '*', same itemtype
1821   * branchcode and itemtype '*'
1822
1823 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1824
1825 =cut
1826
1827 sub GetBranchItemRule {
1828     my ( $branchcode, $itemtype ) = @_;
1829
1830     # Search for rules!
1831     my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
1832         {
1833             branchcode => $branchcode,
1834             itemtype => $itemtype,
1835             rule_name => 'holdallowed',
1836         }
1837     );
1838     my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
1839         {
1840             branchcode => $branchcode,
1841             itemtype => $itemtype,
1842             rule_name => 'hold_fulfillment_policy',
1843         }
1844     );
1845     my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
1846         {
1847             branchcode => $branchcode,
1848             itemtype => $itemtype,
1849             rule_name => 'returnbranch',
1850         }
1851     );
1852
1853     # built-in default circulation rule
1854     my $rules;
1855     $rules->{holdallowed} = defined $holdallowed_rule
1856         ? $holdallowed_rule->rule_value
1857         : 2;
1858     $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
1859         ? $hold_fulfillment_policy_rule->rule_value
1860         : 'any';
1861     $rules->{returnbranch} = defined $returnbranch_rule
1862         ? $returnbranch_rule->rule_value
1863         : 'homebranch';
1864
1865     return $rules;
1866 }
1867
1868 =head2 AddReturn
1869
1870   ($doreturn, $messages, $iteminformation, $borrower) =
1871       &AddReturn( $barcode, $branch [,$exemptfine] [,$returndate] );
1872
1873 Returns a book.
1874
1875 =over 4
1876
1877 =item C<$barcode> is the bar code of the book being returned.
1878
1879 =item C<$branch> is the code of the branch where the book is being returned.
1880
1881 =item C<$exemptfine> indicates that overdue charges for the item will be
1882 removed. Optional.
1883
1884 =item C<$return_date> allows the default return date to be overridden
1885 by the given return date. Optional.
1886
1887 =back
1888
1889 C<&AddReturn> returns a list of four items:
1890
1891 C<$doreturn> is true iff the return succeeded.
1892
1893 C<$messages> is a reference-to-hash giving feedback on the operation.
1894 The keys of the hash are:
1895
1896 =over 4
1897
1898 =item C<BadBarcode>
1899
1900 No item with this barcode exists. The value is C<$barcode>.
1901
1902 =item C<NotIssued>
1903
1904 The book is not currently on loan. The value is C<$barcode>.
1905
1906 =item C<withdrawn>
1907
1908 This book has been withdrawn/cancelled. The value should be ignored.
1909
1910 =item C<Wrongbranch>
1911
1912 This book has was returned to the wrong branch.  The value is a hashref
1913 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1914 contain the branchcode of the incorrect and correct return library, respectively.
1915
1916 =item C<ResFound>
1917
1918 The item was reserved. The value is a reference-to-hash whose keys are
1919 fields from the reserves table of the Koha database, and
1920 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1921 either C<Waiting>, C<Reserved>, or 0.
1922
1923 =item C<WasReturned>
1924
1925 Value 1 if return is successful.
1926
1927 =item C<NeedsTransfer>
1928
1929 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1930
1931 =back
1932
1933 C<$iteminformation> is a reference-to-hash, giving information about the
1934 returned item from the issues table.
1935
1936 C<$borrower> is a reference-to-hash, giving information about the
1937 patron who last borrowed the book.
1938
1939 =cut
1940
1941 sub AddReturn {
1942     my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
1943
1944     if ($branch and not Koha::Libraries->find($branch)) {
1945         warn "AddReturn error: branch '$branch' not found.  Reverting to " . C4::Context->userenv->{'branch'};
1946         undef $branch;
1947     }
1948     $branch = C4::Context->userenv->{'branch'} unless $branch;  # we trust userenv to be a safe fallback/default
1949     my $return_date_specified = !!$return_date;
1950     $return_date //= dt_from_string();
1951     my $messages;
1952     my $patron;
1953     my $doreturn       = 1;
1954     my $validTransfer = 1;
1955     my $stat_type = 'return';
1956
1957     # get information on item
1958     my $item = Koha::Items->find({ barcode => $barcode });
1959     unless ($item) {
1960         return ( 0, { BadBarcode => $barcode } );    # no barcode means no item or borrower.  bail out.
1961     }
1962
1963     my $itemnumber = $item->itemnumber;
1964     my $itemtype = $item->effective_itemtype;
1965
1966     my $issue  = $item->checkout;
1967     if ( $issue ) {
1968         $patron = $issue->patron
1969             or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1970                 . Dumper($issue->unblessed) . "\n";
1971     } else {
1972         $messages->{'NotIssued'} = $barcode;
1973         $item->onloan(undef)->store({skip_record_index=>1}) if defined $item->onloan;
1974
1975         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
1976         $doreturn = 0;
1977         # No issue, no borrowernumber.  ONLY if $doreturn, *might* you have a $borrower later.
1978         # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1979         if (C4::Context->preference("RecordLocalUseOnReturn")) {
1980            $messages->{'LocalUse'} = 1;
1981            $stat_type = 'localuse';
1982         }
1983     }
1984
1985         # full item data, but no borrowernumber or checkout info (no issue)
1986     my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
1987         # get the proper branch to which to return the item
1988     my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
1989         # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1990     my $transfer_trigger = $hbr eq 'homebranch' ? 'ReturnToHome' : $hbr eq 'holdingbranch' ? 'ReturnToHolding' : undef;
1991
1992     my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
1993     my $patron_unblessed = $patron ? $patron->unblessed : {};
1994
1995     my $update_loc_rules = Koha::Config::SysPrefs->find('UpdateItemLocationOnCheckin')->get_yaml_pref_hash();
1996     map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
1997     if ($update_loc_rules) {
1998         if (defined $update_loc_rules->{_ALL_}) {
1999             if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
2000             if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
2001             if ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) {
2002                 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
2003                 $item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1});
2004             }
2005         }
2006         else {
2007             foreach my $key ( keys %$update_loc_rules ) {
2008                 if ( $update_loc_rules->{$key} eq '_PERM_' ) { $update_loc_rules->{$key} = $item->permanent_location; }
2009                 if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
2010                 if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
2011                     $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
2012                     $item->location($update_loc_rules->{$key})->store({skip_record_index=>1});
2013                     last;
2014                 }
2015             }
2016         }
2017     }
2018
2019     my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
2020     if ($yaml) {
2021         $yaml = "$yaml\n\n";  # YAML is anal on ending \n. Surplus does not hurt
2022         my $rules;
2023         eval { $rules = YAML::XS::Load(Encode::encode_utf8($yaml)); };
2024         if ($@) {
2025             warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
2026         }
2027         else {
2028             foreach my $key ( keys %$rules ) {
2029                 if ( $item->notforloan eq $key ) {
2030                     $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
2031                     $item->notforloan($rules->{$key})->store({ log_action => 0, skip_record_index => 1 });
2032                     last;
2033                 }
2034             }
2035         }
2036     }
2037
2038     # check if the return is allowed at this branch
2039     my ($returnallowed, $message) = CanBookBeReturned($item->unblessed, $branch);
2040     unless ($returnallowed){
2041         $messages->{'Wrongbranch'} = {
2042             Wrongbranch => $branch,
2043             Rightbranch => $message
2044         };
2045         $doreturn = 0;
2046         my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2047         $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2048         return ( $doreturn, $messages, $issue, $patron_unblessed);
2049     }
2050
2051     if ( $item->withdrawn ) { # book has been cancelled
2052         $messages->{'withdrawn'} = 1;
2053         $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
2054     }
2055
2056     if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
2057         $doreturn = 0;
2058     }
2059
2060     # case of a return of document (deal with issues and holdingbranch)
2061     if ($doreturn) {
2062         die "The item is not issed and cannot be returned" unless $issue; # Just in case...
2063         $patron or warn "AddReturn without current borrower";
2064
2065         if ($patron) {
2066             eval {
2067                 MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy, { skip_record_index => 1} );
2068             };
2069             unless ( $@ ) {
2070                 if (
2071                     (
2072                         C4::Context->preference('CalculateFinesOnReturn')
2073                         || ( $return_date_specified && C4::Context->preference('CalculateFinesOnBackdate') )
2074                     )
2075                     && !$item->itemlost
2076                   )
2077                 {
2078                     _CalculateAndUpdateFine( { issue => $issue, item => $item->unblessed, borrower => $patron_unblessed, return_date => $return_date } );
2079                 }
2080             } else {
2081                 carp "The checkin for the following issue failed, Please go to the about page and check all messages on the 'System information' to see if there are configuration / data issues ($@)" . Dumper( $issue->unblessed );
2082
2083                 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2084                 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2085
2086                 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
2087             }
2088
2089             # FIXME is the "= 1" right?  This could be the borrower hash.
2090             $messages->{'WasReturned'} = 1;
2091
2092         } else {
2093             $item->onloan(undef)->store({ log_action => 0 , skip_record_index => 1 });
2094         }
2095     }
2096
2097     # the holdingbranch is updated if the document is returned to another location.
2098     # this is always done regardless of whether the item was on loan or not
2099     if ($item->holdingbranch ne $branch) {
2100         $item->holdingbranch($branch)->store({ skip_record_index => 1 });
2101     }
2102
2103     my $item_was_lost = $item->itemlost;
2104     my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
2105     my $updated_item = ModDateLastSeen( $item->itemnumber, $leave_item_lost, { skip_record_index => 1 } ); # will unset itemlost if needed
2106
2107     # fix up the accounts.....
2108     if ($item_was_lost) {
2109         $messages->{'WasLost'} = 1;
2110         unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
2111             $messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded};
2112             $messages->{'LostItemFeeRestored'} = $updated_item->{_restored};
2113
2114             if ( $updated_item->{_charge} ) {
2115                 $issue //= Koha::Old::Checkouts->search(
2116                     { itemnumber => $item->itemnumber },
2117                     { order_by   => { '-desc' => 'returndate' }, rows => 1 } )
2118                   ->single;
2119                 unless ( exists( $patron_unblessed->{branchcode} ) ) {
2120                     my $patron = $issue->patron;
2121                     $patron_unblessed = $patron->unblessed;
2122                 }
2123                 _CalculateAndUpdateFine(
2124                     {
2125                         issue       => $issue,
2126                         item        => $item->unblessed,
2127                         borrower    => $patron_unblessed,
2128                         return_date => $return_date
2129                     }
2130                 );
2131                 _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
2132                     $item->itemnumber, undef, 'RETURNED' );
2133                 $messages->{'LostItemFeeCharged'} = 1;
2134             }
2135         }
2136     }
2137
2138     # check if we have a transfer for this document
2139     my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
2140
2141     # if we have a transfer to complete, we update the line of transfers with the datearrived
2142     my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber );
2143     if ($datesent) {
2144         # At this point we will either fill the transfer or it is a wrong transfer
2145         # either way we should not now generate a new transfer
2146         $validTransfer = 0;
2147         if ( $tobranch eq $branch ) {
2148             my $sth = C4::Context->dbh->prepare(
2149                 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
2150             );
2151             $sth->execute( $item->itemnumber );
2152             $messages->{'TransferArrived'} = $frombranch;
2153         } else {
2154             $messages->{'WrongTransfer'}     = $tobranch;
2155             $messages->{'WrongTransferItem'} = $item->itemnumber;
2156         }
2157     }
2158
2159     # fix up the overdues in accounts...
2160     if ($borrowernumber) {
2161         my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
2162         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, ".$item->itemnumber."...) failed!";  # zero is OK, check defined
2163
2164         if ( $issue and $issue->is_overdue($return_date) ) {
2165         # fix fine days
2166             my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item->unblessed, dt_from_string($issue->date_due), $return_date );
2167             if ($reminder){
2168                 $messages->{'PrevDebarred'} = $debardate;
2169             } else {
2170                 $messages->{'Debarred'} = $debardate if $debardate;
2171             }
2172         # there's no overdue on the item but borrower had been previously debarred
2173         } elsif ( $issue->date_due and $patron->debarred ) {
2174              if ( $patron->debarred eq "9999-12-31") {
2175                 $messages->{'ForeverDebarred'} = $patron->debarred;
2176              } else {
2177                   my $borrower_debar_dt = dt_from_string( $patron->debarred );
2178                   $borrower_debar_dt->truncate(to => 'day');
2179                   my $today_dt = $return_date->clone()->truncate(to => 'day');
2180                   if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2181                       $messages->{'PrevDebarred'} = $patron->debarred;
2182                   }
2183              }
2184         }
2185     }
2186
2187     # find reserves.....
2188     # launch the Checkreserves routine to find any holds
2189     my ($resfound, $resrec);
2190     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2191     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2192     # 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)
2193     if ( $resfound and $resfound eq "Waiting" and $branch ne $resrec->{branchcode} ) {
2194         my $hold = C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
2195         $resfound = 'Reserved';
2196         $resrec = $hold->unblessed;
2197     }
2198     if ($resfound) {
2199           $resrec->{'ResFound'} = $resfound;
2200         $messages->{'ResFound'} = $resrec;
2201     }
2202
2203     # Record the fact that this book was returned.
2204     UpdateStats({
2205         branch         => $branch,
2206         type           => $stat_type,
2207         itemnumber     => $itemnumber,
2208         itemtype       => $itemtype,
2209         location       => $item->location,
2210         borrowernumber => $borrowernumber,
2211         ccode          => $item->ccode,
2212     });
2213
2214     # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2215     if ( $patron ) {
2216         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2217         my %conditions = (
2218             branchcode   => $branch,
2219             categorycode => $patron->categorycode,
2220             item_type    => $itemtype,
2221             notification => 'CHECKIN',
2222         );
2223         if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2224             SendCirculationAlert({
2225                 type     => 'CHECKIN',
2226                 item     => $item->unblessed,
2227                 borrower => $patron->unblessed,
2228                 branch   => $branch,
2229             });
2230         }
2231
2232         logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2233             if C4::Context->preference("ReturnLog");
2234         }
2235
2236     # Check if this item belongs to a biblio record that is attached to an
2237     # ILL request, if it is we need to update the ILL request's status
2238     if ( $doreturn and C4::Context->preference('CirculateILL')) {
2239         my $request = Koha::Illrequests->find(
2240             { biblio_id => $item->biblio->biblionumber }
2241         );
2242         $request->status('RET') if $request;
2243     }
2244
2245     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2246     if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){
2247         my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2248         if  (C4::Context->preference("AutomaticItemReturn"    ) or
2249             (C4::Context->preference("UseBranchTransferLimits") and
2250              ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2251            )) {
2252             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
2253             $debug and warn "item: " . Dumper($item->unblessed);
2254             ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger, { skip_record_index => 1 });
2255             $messages->{'WasTransfered'} = 1;
2256         } else {
2257             $messages->{'NeedsTransfer'} = $returnbranch;
2258             $messages->{'TransferTrigger'} = $transfer_trigger;
2259         }
2260     }
2261
2262     if ( C4::Context->preference('ClaimReturnedLostValue') ) {
2263         my $claims = Koha::Checkouts::ReturnClaims->search(
2264            {
2265                itemnumber => $item->id,
2266                resolution => undef,
2267            }
2268         );
2269
2270         if ( $claims->count ) {
2271             $messages->{ReturnClaims} = $claims;
2272         }
2273     }
2274
2275     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2276     $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2277
2278     if ( $doreturn and $issue ) {
2279         my $checkin = Koha::Old::Checkouts->find($issue->id);
2280
2281         Koha::Plugins->call('after_circ_action', {
2282             action  => 'checkin',
2283             payload => {
2284                 checkout=> $checkin
2285             }
2286         });
2287     }
2288
2289     return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2290 }
2291
2292 =head2 MarkIssueReturned
2293
2294   MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy, [$params] );
2295
2296 Unconditionally marks an issue as being returned by
2297 moving the C<issues> row to C<old_issues> and
2298 setting C<returndate> to the current date.
2299
2300 if C<$returndate> is specified (in iso format), it is used as the date
2301 of the return.
2302
2303 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2304 the old_issue is immediately anonymised
2305
2306 Ideally, this function would be internal to C<C4::Circulation>,
2307 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2308 and offline_circ/process_koc.pl.
2309
2310 The last optional parameter allos passing skip_record_index to the item store call.
2311
2312 =cut
2313
2314 sub MarkIssueReturned {
2315     my ( $borrowernumber, $itemnumber, $returndate, $privacy, $params ) = @_;
2316
2317     # Retrieve the issue
2318     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2319
2320     return unless $issue->borrowernumber == $borrowernumber; # If the item is checked out to another patron we do not return it
2321
2322     my $issue_id = $issue->issue_id;
2323
2324     my $anonymouspatron;
2325     if ( $privacy && $privacy == 2 ) {
2326         # The default of 0 will not work due to foreign key constraints
2327         # The anonymisation will fail if AnonymousPatron is not a valid entry
2328         # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2329         # Note that a warning should appear on the about page (System information tab).
2330         $anonymouspatron = C4::Context->preference('AnonymousPatron');
2331         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."
2332             unless Koha::Patrons->find( $anonymouspatron );
2333     }
2334
2335     my $schema = Koha::Database->schema;
2336
2337     # FIXME Improve the return value and handle it from callers
2338     $schema->txn_do(sub {
2339
2340         my $patron = Koha::Patrons->find( $borrowernumber );
2341
2342         # Update the returndate value
2343         if ( $returndate ) {
2344             $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2345         }
2346         else {
2347             $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2348         }
2349
2350         # Create the old_issues entry
2351         my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2352
2353         # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2354         if ( $privacy && $privacy == 2) {
2355             $old_checkout->borrowernumber($anonymouspatron)->store;
2356         }
2357
2358         # And finally delete the issue
2359         $issue->delete;
2360
2361         $issue->item->onloan(undef)->store({ log_action => 0, skip_record_index => $params->{skip_record_index} });
2362
2363         if ( C4::Context->preference('StoreLastBorrower') ) {
2364             my $item = Koha::Items->find( $itemnumber );
2365             $item->last_returned_by( $patron );
2366         }
2367
2368         # Remove any OVERDUES related debarment if the borrower has no overdues
2369         if ( C4::Context->preference('AutoRemoveOverduesRestrictions')
2370           && $patron->debarred
2371           && !$patron->has_overdues
2372           && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2373         ) {
2374             DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2375         }
2376
2377     });
2378
2379     return $issue_id;
2380 }
2381
2382 =head2 _debar_user_on_return
2383
2384     _debar_user_on_return($borrower, $item, $datedue, $returndate);
2385
2386 C<$borrower> borrower hashref
2387
2388 C<$item> item hashref
2389
2390 C<$datedue> date due DateTime object
2391
2392 C<$returndate> DateTime object representing the return time
2393
2394 Internal function, called only by AddReturn that calculates and updates
2395  the user fine days, and debars them if necessary.
2396
2397 Should only be called for overdue returns
2398
2399 Calculation of the debarment date has been moved to a separate subroutine _calculate_new_debar_dt
2400 to ease testing.
2401
2402 =cut
2403
2404 sub _calculate_new_debar_dt {
2405     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2406
2407     my $branchcode = _GetCircControlBranch( $item, $borrower );
2408     my $circcontrol = C4::Context->preference('CircControl');
2409     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2410         {   categorycode => $borrower->{categorycode},
2411             itemtype     => $item->{itype},
2412             branchcode   => $branchcode,
2413             rules => [
2414                 'finedays',
2415                 'lengthunit',
2416                 'firstremind',
2417                 'maxsuspensiondays',
2418                 'suspension_chargeperiod',
2419             ]
2420         }
2421     );
2422     my $finedays = $issuing_rule ? $issuing_rule->{finedays} : undef;
2423     my $unit     = $issuing_rule ? $issuing_rule->{lengthunit} : undef;
2424     my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2425
2426     return unless $finedays;
2427
2428     # finedays is in days, so hourly loans must multiply by 24
2429     # thus 1 hour late equals 1 day suspension * finedays rate
2430     $finedays = $finedays * 24 if ( $unit eq 'hours' );
2431
2432     # grace period is measured in the same units as the loan
2433     my $grace =
2434       DateTime::Duration->new( $unit => $issuing_rule->{firstremind} // 0);
2435
2436     my $deltadays = DateTime::Duration->new(
2437         days => $chargeable_units
2438     );
2439
2440     if ( $deltadays->subtract($grace)->is_positive() ) {
2441         my $suspension_days = $deltadays * $finedays;
2442
2443         if ( defined $issuing_rule->{suspension_chargeperiod} && $issuing_rule->{suspension_chargeperiod} > 1 ) {
2444             # No need to / 1 and do not consider / 0
2445             $suspension_days = DateTime::Duration->new(
2446                 days => floor( $suspension_days->in_units('days') / $issuing_rule->{suspension_chargeperiod} )
2447             );
2448         }
2449
2450         # If the max suspension days is < than the suspension days
2451         # the suspension days is limited to this maximum period.
2452         my $max_sd = $issuing_rule->{maxsuspensiondays};
2453         if ( defined $max_sd && $max_sd ne '' ) {
2454             $max_sd = DateTime::Duration->new( days => $max_sd );
2455             $suspension_days = $max_sd
2456               if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2457         }
2458
2459         my ( $has_been_extended );
2460         if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2461             my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2462             if ( $debarment ) {
2463                 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2464                 $has_been_extended = 1;
2465             }
2466         }
2467
2468         my $new_debar_dt;
2469         # Use the calendar or not to calculate the debarment date
2470         if ( C4::Context->preference('SuspensionsCalendar') eq 'noSuspensionsWhenClosed' ) {
2471             my $calendar = Koha::Calendar->new(
2472                 branchcode => $branchcode,
2473                 days_mode  => 'Calendar'
2474             );
2475             $new_debar_dt = $calendar->addDuration( $return_date, $suspension_days );
2476         }
2477         else {
2478             $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
2479         }
2480         return $new_debar_dt;
2481     }
2482     return;
2483 }
2484
2485 sub _debar_user_on_return {
2486     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2487
2488     $return_date //= dt_from_string();
2489
2490     my $new_debar_dt = _calculate_new_debar_dt ($borrower, $item, $dt_due, $return_date);
2491
2492     return unless $new_debar_dt;
2493
2494     Koha::Patron::Debarments::AddUniqueDebarment({
2495         borrowernumber => $borrower->{borrowernumber},
2496         expiration     => $new_debar_dt->ymd(),
2497         type           => 'SUSPENSION',
2498     });
2499     # if borrower was already debarred but does not get an extra debarment
2500     my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2501     my ($new_debarment_str, $is_a_reminder);
2502     if ( $borrower->{debarred} && $borrower->{debarred} eq $patron->is_debarred ) {
2503         $is_a_reminder = 1;
2504         $new_debarment_str = $borrower->{debarred};
2505     } else {
2506         $new_debarment_str = $new_debar_dt->ymd();
2507     }
2508     # FIXME Should return a DateTime object
2509     return $new_debarment_str, $is_a_reminder;
2510 }
2511
2512 =head2 _FixOverduesOnReturn
2513
2514    &_FixOverduesOnReturn($borrowernumber, $itemnumber, $exemptfine, $status);
2515
2516 C<$borrowernumber> borrowernumber
2517
2518 C<$itemnumber> itemnumber
2519
2520 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2521
2522 C<$status> ENUM -- reason for fix [ RETURNED, RENEWED, LOST, FORGIVEN ]
2523
2524 Internal function
2525
2526 =cut
2527
2528 sub _FixOverduesOnReturn {
2529     my ( $borrowernumber, $item, $exemptfine, $status ) = @_;
2530     unless( $borrowernumber ) {
2531         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2532         return;
2533     }
2534     unless( $item ) {
2535         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2536         return;
2537     }
2538     unless( $status ) {
2539         warn "_FixOverduesOnReturn() not supplied valid status";
2540         return;
2541     }
2542
2543     my $schema = Koha::Database->schema;
2544
2545     my $result = $schema->txn_do(
2546         sub {
2547             # check for overdue fine
2548             my $accountlines = Koha::Account::Lines->search(
2549                 {
2550                     borrowernumber  => $borrowernumber,
2551                     itemnumber      => $item,
2552                     debit_type_code => 'OVERDUE',
2553                     status          => 'UNRETURNED'
2554                 }
2555             );
2556             return 0 unless $accountlines->count; # no warning, there's just nothing to fix
2557
2558             my $accountline = $accountlines->next;
2559             my $payments = $accountline->credits;
2560
2561             my $amountoutstanding = $accountline->amountoutstanding;
2562             if ( $accountline->amount == 0 && $payments->count == 0 ) {
2563                 $accountline->delete;
2564                 return 0; # no warning, we've just removed a zero value fine (backdated return)
2565             } elsif ($exemptfine && ($amountoutstanding != 0)) {
2566                 my $account = Koha::Account->new({patron_id => $borrowernumber});
2567                 my $credit = $account->add_credit(
2568                     {
2569                         amount     => $amountoutstanding,
2570                         user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
2571                         library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
2572                         interface  => C4::Context->interface,
2573                         type       => 'FORGIVEN',
2574                         item_id    => $item
2575                     }
2576                 );
2577
2578                 $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
2579
2580                 if (C4::Context->preference("FinesLog")) {
2581                     &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2582                 }
2583             }
2584
2585             $accountline->status($status);
2586             return $accountline->store();
2587         }
2588     );
2589
2590     return $result;
2591 }
2592
2593 =head2 _GetCircControlBranch
2594
2595    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2596
2597 Internal function : 
2598
2599 Return the library code to be used to determine which circulation
2600 policy applies to a transaction.  Looks up the CircControl and
2601 HomeOrHoldingBranch system preferences.
2602
2603 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2604
2605 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2606
2607 =cut
2608
2609 sub _GetCircControlBranch {
2610     my ($item, $borrower) = @_;
2611     my $circcontrol = C4::Context->preference('CircControl');
2612     my $branch;
2613
2614     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2615         $branch= C4::Context->userenv->{'branch'};
2616     } elsif ($circcontrol eq 'PatronLibrary') {
2617         $branch=$borrower->{branchcode};
2618     } else {
2619         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2620         $branch = $item->{$branchfield};
2621         # default to item home branch if holdingbranch is used
2622         # and is not defined
2623         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2624             $branch = $item->{homebranch};
2625         }
2626     }
2627     return $branch;
2628 }
2629
2630 =head2 GetOpenIssue
2631
2632   $issue = GetOpenIssue( $itemnumber );
2633
2634 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2635
2636 C<$itemnumber> is the item's itemnumber
2637
2638 Returns a hashref
2639
2640 =cut
2641
2642 sub GetOpenIssue {
2643   my ( $itemnumber ) = @_;
2644   return unless $itemnumber;
2645   my $dbh = C4::Context->dbh;  
2646   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2647   $sth->execute( $itemnumber );
2648   return $sth->fetchrow_hashref();
2649
2650 }
2651
2652 =head2 GetUpcomingDueIssues
2653
2654   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2655
2656 =cut
2657
2658 sub GetUpcomingDueIssues {
2659     my $params = shift;
2660
2661     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2662     my $dbh = C4::Context->dbh;
2663     my $statement;
2664     $statement = q{
2665         SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2666         FROM issues
2667         LEFT JOIN items USING (itemnumber)
2668         LEFT JOIN branches ON branches.branchcode =
2669     };
2670     $statement .= $params->{'owning_library'} ? " items.homebranch " : " issues.branchcode ";
2671     $statement .= " WHERE returndate is NULL AND TO_DAYS( date_due )-TO_DAYS( NOW() ) BETWEEN 0 AND ?";
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->addDuration( $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