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