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