Bug 12556: Add new "in processing" state to holds
[koha.git] / C4 / Circulation.pm
1 package C4::Circulation;
2
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22 use DateTime;
23 use POSIX qw( floor );
24 use Koha::DateUtils;
25 use C4::Context;
26 use C4::Stats;
27 use C4::Reserves;
28 use C4::Biblio;
29 use C4::Items;
30 use C4::Members;
31 use C4::Accounts;
32 use C4::ItemCirculationAlertPreference;
33 use C4::Message;
34 use C4::Debug;
35 use C4::Log; # logaction
36 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
37 use C4::RotatingCollections qw(GetCollectionItemBranches);
38 use Algorithm::CheckDigits;
39
40 use Data::Dumper;
41 use Koha::Account;
42 use Koha::AuthorisedValues;
43 use Koha::Biblioitems;
44 use Koha::DateUtils;
45 use Koha::Calendar;
46 use Koha::Checkouts;
47 use Koha::Illrequests;
48 use Koha::Items;
49 use Koha::Patrons;
50 use Koha::Patron::Debarments;
51 use Koha::Database;
52 use Koha::Libraries;
53 use Koha::Account::Lines;
54 use Koha::Holds;
55 use Koha::Account::Lines;
56 use Koha::Account::Offsets;
57 use Koha::Config::SysPrefs;
58 use Koha::Charges::Fees;
59 use Koha::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 do, 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         } else {
2068             $messages->{'WrongTransfer'}     = $tobranch;
2069             $messages->{'WrongTransferItem'} = $item->itemnumber;
2070         }
2071     }
2072
2073     # fix up the overdues in accounts...
2074     if ($borrowernumber) {
2075         my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
2076         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, ".$item->itemnumber."...) failed!";  # zero is OK, check defined
2077
2078         if ( $issue and $issue->is_overdue($return_date) ) {
2079         # fix fine days
2080             my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item->unblessed, dt_from_string($issue->date_due), $return_date );
2081             if ($reminder){
2082                 $messages->{'PrevDebarred'} = $debardate;
2083             } else {
2084                 $messages->{'Debarred'} = $debardate if $debardate;
2085             }
2086         # there's no overdue on the item but borrower had been previously debarred
2087         } elsif ( $issue->date_due and $patron->debarred ) {
2088              if ( $patron->debarred eq "9999-12-31") {
2089                 $messages->{'ForeverDebarred'} = $patron->debarred;
2090              } else {
2091                   my $borrower_debar_dt = dt_from_string( $patron->debarred );
2092                   $borrower_debar_dt->truncate(to => 'day');
2093                   my $today_dt = $return_date->clone()->truncate(to => 'day');
2094                   if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2095                       $messages->{'PrevDebarred'} = $patron->debarred;
2096                   }
2097              }
2098         }
2099     }
2100
2101     # find reserves.....
2102     # launch the Checkreserves routine to find any holds
2103     my ($resfound, $resrec);
2104     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2105     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2106     # 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)
2107     if ( $resfound and $resfound eq "Waiting" and $branch ne $resrec->{branchcode} ) {
2108         my $hold = C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
2109         $resfound = 'Reserved';
2110         $resrec = $hold->unblessed;
2111     }
2112     if ($resfound) {
2113           $resrec->{'ResFound'} = $resfound;
2114         $messages->{'ResFound'} = $resrec;
2115     }
2116
2117     # Record the fact that this book was returned.
2118     UpdateStats({
2119         branch         => $branch,
2120         type           => $stat_type,
2121         itemnumber     => $itemnumber,
2122         itemtype       => $itemtype,
2123         location       => $item->location,
2124         borrowernumber => $borrowernumber,
2125         ccode          => $item->ccode,
2126     });
2127
2128     # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2129     if ( $patron ) {
2130         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2131         my %conditions = (
2132             branchcode   => $branch,
2133             categorycode => $patron->categorycode,
2134             item_type    => $itemtype,
2135             notification => 'CHECKIN',
2136         );
2137         if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2138             SendCirculationAlert({
2139                 type     => 'CHECKIN',
2140                 item     => $item->unblessed,
2141                 borrower => $patron->unblessed,
2142                 branch   => $branch,
2143             });
2144         }
2145
2146         logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2147             if C4::Context->preference("ReturnLog");
2148         }
2149
2150     # Check if this item belongs to a biblio record that is attached to an
2151     # ILL request, if it is we need to update the ILL request's status
2152     if ( $doreturn and C4::Context->preference('CirculateILL')) {
2153         my $request = Koha::Illrequests->find(
2154             { biblio_id => $item->biblio->biblionumber }
2155         );
2156         $request->status('RET') if $request;
2157     }
2158
2159     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2160     if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){
2161         my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2162         if  (C4::Context->preference("AutomaticItemReturn"    ) or
2163             (C4::Context->preference("UseBranchTransferLimits") and
2164              ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2165            )) {
2166             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
2167             $debug and warn "item: " . Dumper($item->unblessed);
2168             ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger);
2169             $messages->{'WasTransfered'} = 1;
2170         } else {
2171             $messages->{'NeedsTransfer'} = $returnbranch;
2172             $messages->{'TransferTrigger'} = $transfer_trigger;
2173         }
2174     }
2175
2176     if ( C4::Context->preference('ClaimReturnedLostValue') ) {
2177         my $claims = Koha::Checkouts::ReturnClaims->search(
2178            {
2179                itemnumber => $item->id,
2180                resolution => undef,
2181            }
2182         );
2183
2184         if ( $claims->count ) {
2185             $messages->{ReturnClaims} = $claims;
2186         }
2187     }
2188
2189     if ( $doreturn and $issue ) {
2190         my $checkin = Koha::Old::Checkouts->find($issue->id);
2191
2192         Koha::Plugins->call('after_circ_action', {
2193             action  => 'checkin',
2194             payload => {
2195                 checkout=> $checkin
2196             }
2197         });
2198     }
2199
2200     return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2201 }
2202
2203 =head2 MarkIssueReturned
2204
2205   MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy);
2206
2207 Unconditionally marks an issue as being returned by
2208 moving the C<issues> row to C<old_issues> and
2209 setting C<returndate> to the current date.
2210
2211 if C<$returndate> is specified (in iso format), it is used as the date
2212 of the return.
2213
2214 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2215 the old_issue is immediately anonymised
2216
2217 Ideally, this function would be internal to C<C4::Circulation>,
2218 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2219 and offline_circ/process_koc.pl.
2220
2221 =cut
2222
2223 sub MarkIssueReturned {
2224     my ( $borrowernumber, $itemnumber, $returndate, $privacy ) = @_;
2225
2226     # Retrieve the issue
2227     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2228
2229     return unless $issue->borrowernumber == $borrowernumber; # If the item is checked out to another patron we do not return it
2230
2231     my $issue_id = $issue->issue_id;
2232
2233     my $anonymouspatron;
2234     if ( $privacy && $privacy == 2 ) {
2235         # The default of 0 will not work due to foreign key constraints
2236         # The anonymisation will fail if AnonymousPatron is not a valid entry
2237         # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2238         # Note that a warning should appear on the about page (System information tab).
2239         $anonymouspatron = C4::Context->preference('AnonymousPatron');
2240         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."
2241             unless Koha::Patrons->find( $anonymouspatron );
2242     }
2243
2244     my $schema = Koha::Database->schema;
2245
2246     # FIXME Improve the return value and handle it from callers
2247     $schema->txn_do(sub {
2248
2249         my $patron = Koha::Patrons->find( $borrowernumber );
2250
2251         # Update the returndate value
2252         if ( $returndate ) {
2253             $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2254         }
2255         else {
2256             $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2257         }
2258
2259         # Create the old_issues entry
2260         my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2261
2262         # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2263         if ( $privacy && $privacy == 2) {
2264             $old_checkout->borrowernumber($anonymouspatron)->store;
2265         }
2266
2267         # And finally delete the issue
2268         $issue->delete;
2269
2270         $issue->item->onloan(undef)->store({ log_action => 0 });
2271
2272         if ( C4::Context->preference('StoreLastBorrower') ) {
2273             my $item = Koha::Items->find( $itemnumber );
2274             $item->last_returned_by( $patron );
2275         }
2276
2277         # Remove any OVERDUES related debarment if the borrower has no overdues
2278         if ( C4::Context->preference('AutoRemoveOverduesRestrictions')
2279           && $patron->debarred
2280           && !$patron->has_overdues
2281           && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2282         ) {
2283             DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2284         }
2285
2286     });
2287
2288     return $issue_id;
2289 }
2290
2291 =head2 _debar_user_on_return
2292
2293     _debar_user_on_return($borrower, $item, $datedue, $returndate);
2294
2295 C<$borrower> borrower hashref
2296
2297 C<$item> item hashref
2298
2299 C<$datedue> date due DateTime object
2300
2301 C<$returndate> DateTime object representing the return time
2302
2303 Internal function, called only by AddReturn that calculates and updates
2304  the user fine days, and debars them if necessary.
2305
2306 Should only be called for overdue returns
2307
2308 Calculation of the debarment date has been moved to a separate subroutine _calculate_new_debar_dt
2309 to ease testing.
2310
2311 =cut
2312
2313 sub _calculate_new_debar_dt {
2314     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2315
2316     my $branchcode = _GetCircControlBranch( $item, $borrower );
2317     my $circcontrol = C4::Context->preference('CircControl');
2318     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2319         {   categorycode => $borrower->{categorycode},
2320             itemtype     => $item->{itype},
2321             branchcode   => $branchcode,
2322             rules => [
2323                 'finedays',
2324                 'lengthunit',
2325                 'firstremind',
2326                 'maxsuspensiondays',
2327                 'suspension_chargeperiod',
2328             ]
2329         }
2330     );
2331     my $finedays = $issuing_rule ? $issuing_rule->{finedays} : undef;
2332     my $unit     = $issuing_rule ? $issuing_rule->{lengthunit} : undef;
2333     my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2334
2335     return unless $finedays;
2336
2337     # finedays is in days, so hourly loans must multiply by 24
2338     # thus 1 hour late equals 1 day suspension * finedays rate
2339     $finedays = $finedays * 24 if ( $unit eq 'hours' );
2340
2341     # grace period is measured in the same units as the loan
2342     my $grace =
2343       DateTime::Duration->new( $unit => $issuing_rule->{firstremind} );
2344
2345     my $deltadays = DateTime::Duration->new(
2346         days => $chargeable_units
2347     );
2348
2349     if ( $deltadays->subtract($grace)->is_positive() ) {
2350         my $suspension_days = $deltadays * $finedays;
2351
2352         if ( defined $issuing_rule->{suspension_chargeperiod} && $issuing_rule->{suspension_chargeperiod} > 1 ) {
2353             # No need to / 1 and do not consider / 0
2354             $suspension_days = DateTime::Duration->new(
2355                 days => floor( $suspension_days->in_units('days') / $issuing_rule->{suspension_chargeperiod} )
2356             );
2357         }
2358
2359         # If the max suspension days is < than the suspension days
2360         # the suspension days is limited to this maximum period.
2361         my $max_sd = $issuing_rule->{maxsuspensiondays};
2362         if ( defined $max_sd && $max_sd ne '' ) {
2363             $max_sd = DateTime::Duration->new( days => $max_sd );
2364             $suspension_days = $max_sd
2365               if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2366         }
2367
2368         my ( $has_been_extended );
2369         if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2370             my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2371             if ( $debarment ) {
2372                 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2373                 $has_been_extended = 1;
2374             }
2375         }
2376
2377         my $new_debar_dt;
2378         # Use the calendar or not to calculate the debarment date
2379         if ( C4::Context->preference('SuspensionsCalendar') eq 'noSuspensionsWhenClosed' ) {
2380             my $calendar = Koha::Calendar->new(
2381                 branchcode => $branchcode,
2382                 days_mode  => 'Calendar'
2383             );
2384             $new_debar_dt = $calendar->addDate( $return_date, $suspension_days );
2385         }
2386         else {
2387             $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
2388         }
2389         return $new_debar_dt;
2390     }
2391     return;
2392 }
2393
2394 sub _debar_user_on_return {
2395     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2396
2397     $return_date //= dt_from_string();
2398
2399     my $new_debar_dt = _calculate_new_debar_dt ($borrower, $item, $dt_due, $return_date);
2400
2401     return unless $new_debar_dt;
2402
2403     Koha::Patron::Debarments::AddUniqueDebarment({
2404         borrowernumber => $borrower->{borrowernumber},
2405         expiration     => $new_debar_dt->ymd(),
2406         type           => 'SUSPENSION',
2407     });
2408     # if borrower was already debarred but does not get an extra debarment
2409     my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2410     my ($new_debarment_str, $is_a_reminder);
2411     if ( $borrower->{debarred} && $borrower->{debarred} eq $patron->is_debarred ) {
2412         $is_a_reminder = 1;
2413         $new_debarment_str = $borrower->{debarred};
2414     } else {
2415         $new_debarment_str = $new_debar_dt->ymd();
2416     }
2417     # FIXME Should return a DateTime object
2418     return $new_debarment_str, $is_a_reminder;
2419 }
2420
2421 =head2 _FixOverduesOnReturn
2422
2423    &_FixOverduesOnReturn($borrowernumber, $itemnumber, $exemptfine, $status);
2424
2425 C<$borrowernumber> borrowernumber
2426
2427 C<$itemnumber> itemnumber
2428
2429 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2430
2431 C<$status> ENUM -- reason for fix [ RETURNED, RENEWED, LOST, FORGIVEN ]
2432
2433 Internal function
2434
2435 =cut
2436
2437 sub _FixOverduesOnReturn {
2438     my ( $borrowernumber, $item, $exemptfine, $status ) = @_;
2439     unless( $borrowernumber ) {
2440         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2441         return;
2442     }
2443     unless( $item ) {
2444         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2445         return;
2446     }
2447     unless( $status ) {
2448         warn "_FixOverduesOnReturn() not supplied valid status";
2449         return;
2450     }
2451
2452     my $schema = Koha::Database->schema;
2453
2454     my $result = $schema->txn_do(
2455         sub {
2456             # check for overdue fine
2457             my $accountlines = Koha::Account::Lines->search(
2458                 {
2459                     borrowernumber  => $borrowernumber,
2460                     itemnumber      => $item,
2461                     debit_type_code => 'OVERDUE',
2462                     status          => 'UNRETURNED'
2463                 }
2464             );
2465             return 0 unless $accountlines->count; # no warning, there's just nothing to fix
2466
2467             my $accountline = $accountlines->next;
2468             my $payments = $accountline->credits;
2469
2470             my $amountoutstanding = $accountline->amountoutstanding;
2471             if ( $accountline->amount == 0 && $payments->count == 0 ) {
2472                 $accountline->delete;
2473             } elsif ($exemptfine && ($amountoutstanding != 0)) {
2474                 my $account = Koha::Account->new({patron_id => $borrowernumber});
2475                 my $credit = $account->add_credit(
2476                     {
2477                         amount     => $amountoutstanding,
2478                         user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
2479                         library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
2480                         interface  => C4::Context->interface,
2481                         type       => 'FORGIVEN',
2482                         item_id    => $item
2483                     }
2484                 );
2485
2486                 $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
2487
2488                 if (C4::Context->preference("FinesLog")) {
2489                     &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2490                 }
2491
2492                 $accountline->status('FORGIVEN');
2493                 $accountline->store();
2494             } else {
2495                 $accountline->status($status);
2496                 $accountline->store();
2497
2498             }
2499         }
2500     );
2501
2502     return $result;
2503 }
2504
2505 =head2 _GetCircControlBranch
2506
2507    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2508
2509 Internal function : 
2510
2511 Return the library code to be used to determine which circulation
2512 policy applies to a transaction.  Looks up the CircControl and
2513 HomeOrHoldingBranch system preferences.
2514
2515 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2516
2517 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2518
2519 =cut
2520
2521 sub _GetCircControlBranch {
2522     my ($item, $borrower) = @_;
2523     my $circcontrol = C4::Context->preference('CircControl');
2524     my $branch;
2525
2526     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2527         $branch= C4::Context->userenv->{'branch'};
2528     } elsif ($circcontrol eq 'PatronLibrary') {
2529         $branch=$borrower->{branchcode};
2530     } else {
2531         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2532         $branch = $item->{$branchfield};
2533         # default to item home branch if holdingbranch is used
2534         # and is not defined
2535         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2536             $branch = $item->{homebranch};
2537         }
2538     }
2539     return $branch;
2540 }
2541
2542 =head2 GetOpenIssue
2543
2544   $issue = GetOpenIssue( $itemnumber );
2545
2546 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2547
2548 C<$itemnumber> is the item's itemnumber
2549
2550 Returns a hashref
2551
2552 =cut
2553
2554 sub GetOpenIssue {
2555   my ( $itemnumber ) = @_;
2556   return unless $itemnumber;
2557   my $dbh = C4::Context->dbh;  
2558   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2559   $sth->execute( $itemnumber );
2560   return $sth->fetchrow_hashref();
2561
2562 }
2563
2564 =head2 GetBiblioIssues
2565
2566   $issues = GetBiblioIssues($biblionumber);
2567
2568 this function get all issues from a biblionumber.
2569
2570 Return:
2571 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash contains all column from
2572 tables issues and the firstname,surname & cardnumber from borrowers.
2573
2574 =cut
2575
2576 sub GetBiblioIssues {
2577     my $biblionumber = shift;
2578     return unless $biblionumber;
2579     my $dbh   = C4::Context->dbh;
2580     my $query = "
2581         SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2582         FROM issues
2583             LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2584             LEFT JOIN items ON issues.itemnumber = items.itemnumber
2585             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2586             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2587         WHERE biblio.biblionumber = ?
2588         UNION ALL
2589         SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2590         FROM old_issues
2591             LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2592             LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2593             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2594             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2595         WHERE biblio.biblionumber = ?
2596         ORDER BY timestamp
2597     ";
2598     my $sth = $dbh->prepare($query);
2599     $sth->execute($biblionumber, $biblionumber);
2600
2601     my @issues;
2602     while ( my $data = $sth->fetchrow_hashref ) {
2603         push @issues, $data;
2604     }
2605     return \@issues;
2606 }
2607
2608 =head2 GetUpcomingDueIssues
2609
2610   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2611
2612 =cut
2613
2614 sub GetUpcomingDueIssues {
2615     my $params = shift;
2616
2617     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2618     my $dbh = C4::Context->dbh;
2619
2620     my $statement = <<END_SQL;
2621 SELECT *
2622 FROM (
2623     SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2624     FROM issues
2625     LEFT JOIN items USING (itemnumber)
2626     LEFT OUTER JOIN branches USING (branchcode)
2627     WHERE returndate is NULL
2628 ) tmp
2629 WHERE days_until_due >= 0 AND days_until_due <= ?
2630 END_SQL
2631
2632     my @bind_parameters = ( $params->{'days_in_advance'} );
2633     
2634     my $sth = $dbh->prepare( $statement );
2635     $sth->execute( @bind_parameters );
2636     my $upcoming_dues = $sth->fetchall_arrayref({});
2637
2638     return $upcoming_dues;
2639 }
2640
2641 =head2 CanBookBeRenewed
2642
2643   ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2644
2645 Find out whether a borrowed item may be renewed.
2646
2647 C<$borrowernumber> is the borrower number of the patron who currently
2648 has the item on loan.
2649
2650 C<$itemnumber> is the number of the item to renew.
2651
2652 C<$override_limit>, if supplied with a true value, causes
2653 the limit on the number of times that the loan can be renewed
2654 (as controlled by the item type) to be ignored. Overriding also allows
2655 to renew sooner than "No renewal before" and to manually renew loans
2656 that are automatically renewed.
2657
2658 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2659 item must currently be on loan to the specified borrower; renewals
2660 must be allowed for the item's type; and the borrower must not have
2661 already renewed the loan. $error will contain the reason the renewal can not proceed
2662
2663 =cut
2664
2665 sub CanBookBeRenewed {
2666     my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_;
2667
2668     my $dbh    = C4::Context->dbh;
2669     my $renews = 1;
2670     my $auto_renew = "no";
2671
2672     my $item      = Koha::Items->find($itemnumber)      or return ( 0, 'no_item' );
2673     my $issue = $item->checkout or return ( 0, 'no_checkout' );
2674     return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2675     return ( 0, 'item_denied_renewal') if _item_denied_renewal({ item => $item });
2676
2677     my $patron = $issue->patron or return;
2678
2679     # override_limit will override anything else except on_reserve
2680     unless ( $override_limit ){
2681         my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
2682         my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2683             {
2684                 categorycode => $patron->categorycode,
2685                 itemtype     => $item->effective_itemtype,
2686                 branchcode   => $branchcode,
2687                 rules => [
2688                     'renewalsallowed',
2689                     'no_auto_renewal_after',
2690                     'no_auto_renewal_after_hard_limit',
2691                     'lengthunit',
2692                     'norenewalbefore',
2693                 ]
2694             }
2695         );
2696
2697         return ( 0, "too_many" )
2698           if not $issuing_rule->{renewalsallowed} or $issuing_rule->{renewalsallowed} <= $issue->renewals;
2699
2700         my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2701         my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2702         $patron         = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2703         my $restricted  = $patron->is_debarred;
2704         my $hasoverdues = $patron->has_overdues;
2705
2706         if ( $restricted and $restrictionblockrenewing ) {
2707             return ( 0, 'restriction');
2708         } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2709             return ( 0, 'overdue');
2710         }
2711
2712         if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2713
2714             if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2715                 return ( 0, 'auto_account_expired' );
2716             }
2717
2718             if ( defined $issuing_rule->{no_auto_renewal_after}
2719                     and $issuing_rule->{no_auto_renewal_after} ne "" ) {
2720                 # Get issue_date and add no_auto_renewal_after
2721                 # If this is greater than today, it's too late for renewal.
2722                 my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2723                 $maximum_renewal_date->add(
2724                     $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after}
2725                 );
2726                 my $now = dt_from_string;
2727                 if ( $now >= $maximum_renewal_date ) {
2728                     return ( 0, "auto_too_late" );
2729                 }
2730             }
2731             if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit}
2732                           and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) {
2733                 # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2734                 if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) {
2735                     return ( 0, "auto_too_late" );
2736                 }
2737             }
2738
2739             if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2740                 my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2741                 my $amountoutstanding =
2742                   C4::Context->preference("OPACFineNoRenewalsIncludeCredit")
2743                   ? $patron->account->balance
2744                   : $patron->account->outstanding_debits->total_outstanding;
2745                 if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2746                     return ( 0, "auto_too_much_oweing" );
2747                 }
2748             }
2749         }
2750
2751         if ( defined $issuing_rule->{norenewalbefore}
2752             and $issuing_rule->{norenewalbefore} ne "" )
2753         {
2754
2755             # Calculate soonest renewal by subtracting 'No renewal before' from due date
2756             my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2757                 $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
2758
2759             # Depending on syspref reset the exact time, only check the date
2760             if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2761                 and $issuing_rule->{lengthunit} eq 'days' )
2762             {
2763                 $soonestrenewal->truncate( to => 'day' );
2764             }
2765
2766             if ( $soonestrenewal > dt_from_string() )
2767             {
2768                 $auto_renew = ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon";
2769             }
2770             elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2771                 $auto_renew = "ok";
2772             }
2773         }
2774
2775         # Fallback for automatic renewals:
2776         # If norenewalbefore is undef, don't renew before due date.
2777         if ( $issue->auto_renew && $auto_renew eq "no" && $patron->autorenew_checkouts ) {
2778             my $now = dt_from_string;
2779             if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
2780                 $auto_renew = "ok";
2781             } else {
2782                 $auto_renew = "auto_too_soon";
2783             }
2784         }
2785     }
2786
2787     my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2788
2789     # If next hold is non priority, then check if any hold with priority (non_priority = 0) exists for the same biblionumber.
2790     if ( $resfound && $resrec->{non_priority} ) {
2791         $resfound = Koha::Holds->search(
2792             { biblionumber => $resrec->{biblionumber}, non_priority => 0 } )
2793           ->count > 0;
2794     }
2795
2796
2797
2798     # This item can fill one or more unfilled reserve, can those unfilled reserves
2799     # all be filled by other available items?
2800     if ( $resfound
2801         && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2802     {
2803         my $schema = Koha::Database->new()->schema();
2804
2805         my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2806         if ($item_holds) {
2807             # There is an item level hold on this item, no other item can fill the hold
2808             $resfound = 1;
2809         }
2810         else {
2811
2812             # Get all other items that could possibly fill reserves
2813             my @itemnumbers = $schema->resultset('Item')->search(
2814                 {
2815                     biblionumber => $resrec->{biblionumber},
2816                     onloan       => undef,
2817                     notforloan   => 0,
2818                     -not         => { itemnumber => $itemnumber }
2819                 },
2820                 { columns => 'itemnumber' }
2821             )->get_column('itemnumber')->all();
2822
2823             # Get all other reserves that could have been filled by this item
2824             my @borrowernumbers;
2825             while (1) {
2826                 my ( $reserve_found, $reserve, undef ) =
2827                   C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2828
2829                 if ($reserve_found) {
2830                     push( @borrowernumbers, $reserve->{borrowernumber} );
2831                 }
2832                 else {
2833                     last;
2834                 }
2835             }
2836
2837             # If the count of the union of the lists of reservable items for each borrower
2838             # is equal or greater than the number of borrowers, we know that all reserves
2839             # can be filled with available items. We can get the union of the sets simply
2840             # by pushing all the elements onto an array and removing the duplicates.
2841             my @reservable;
2842             my %patrons;
2843             ITEM: foreach my $itemnumber (@itemnumbers) {
2844                 my $item = Koha::Items->find( $itemnumber );
2845                 next if IsItemOnHoldAndFound( $itemnumber );
2846                 for my $borrowernumber (@borrowernumbers) {
2847                     my $patron = $patrons{$borrowernumber} //= Koha::Patrons->find( $borrowernumber );
2848                     next unless IsAvailableForItemLevelRequest($item, $patron);
2849                     next unless CanItemBeReserved($borrowernumber,$itemnumber);
2850
2851                     push @reservable, $itemnumber;
2852                     if (@reservable >= @borrowernumbers) {
2853                         $resfound = 0;
2854                         last ITEM;
2855                     }
2856                     last;
2857                 }
2858             }
2859         }
2860     }
2861     if( $cron ) { #The cron wants to return 'too_soon' over 'on_reserve'
2862         return ( 0, $auto_renew  ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2863         return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2864     } else { # For other purposes we want 'on_reserve' before 'too_soon'
2865         return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2866         return ( 0, $auto_renew  ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2867     }
2868
2869     return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed
2870
2871     return ( 1, undef );
2872 }
2873
2874 =head2 AddRenewal
2875
2876   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2877
2878 Renews a loan.
2879
2880 C<$borrowernumber> is the borrower number of the patron who currently
2881 has the item.
2882
2883 C<$itemnumber> is the number of the item to renew.
2884
2885 C<$branch> is the library where the renewal took place (if any).
2886            The library that controls the circ policies for the renewal is retrieved from the issues record.
2887
2888 C<$datedue> can be a DateTime object used to set the due date.
2889
2890 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2891 this parameter is not supplied, lastreneweddate is set to the current date.
2892
2893 C<$skipfinecalc> is an optional boolean. There may be circumstances where, even if the
2894 CalculateFinesOnReturn syspref is enabled, we don't want to calculate fines upon renew,
2895 for example, when we're renewing as a result of a fine being paid (see RenewAccruingItemWhenPaid
2896 syspref)
2897
2898 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2899 from the book's item type.
2900
2901 =cut
2902
2903 sub AddRenewal {
2904     my $borrowernumber  = shift;
2905     my $itemnumber      = shift or return;
2906     my $branch          = shift;
2907     my $datedue         = shift;
2908     my $lastreneweddate = shift || dt_from_string();
2909     my $skipfinecalc    = shift;
2910
2911     my $item_object   = Koha::Items->find($itemnumber) or return;
2912     my $biblio = $item_object->biblio;
2913     my $issue  = $item_object->checkout;
2914     my $item_unblessed = $item_object->unblessed;
2915
2916     my $dbh = C4::Context->dbh;
2917
2918     return unless $issue;
2919
2920     $borrowernumber ||= $issue->borrowernumber;
2921
2922     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2923         carp 'Invalid date passed to AddRenewal.';
2924         return;
2925     }
2926
2927     my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
2928     my $patron_unblessed = $patron->unblessed;
2929
2930     my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
2931
2932     my $schema = Koha::Database->schema;
2933     $schema->txn_do(sub{
2934
2935         if ( !$skipfinecalc && C4::Context->preference('CalculateFinesOnReturn') ) {
2936             _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed } );
2937         }
2938         _FixOverduesOnReturn( $borrowernumber, $itemnumber, undef, 'RENEWED' );
2939
2940         # If the due date wasn't specified, calculate it by adding the
2941         # book's loan length to today's date or the current due date
2942         # based on the value of the RenewalPeriodBase syspref.
2943         my $itemtype = $item_object->effective_itemtype;
2944         unless ($datedue) {
2945
2946             $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2947                                             dt_from_string( $issue->date_due, 'sql' ) :
2948                                             dt_from_string();
2949             $datedue =  CalcDateDue($datedue, $itemtype, $circ_library->branchcode, $patron_unblessed, 'is a renewal');
2950         }
2951
2952         my $fees = Koha::Charges::Fees->new(
2953             {
2954                 patron    => $patron,
2955                 library   => $circ_library,
2956                 item      => $item_object,
2957                 from_date => dt_from_string( $issue->date_due, 'sql' ),
2958                 to_date   => dt_from_string($datedue),
2959             }
2960         );
2961
2962         # Update the issues record to have the new due date, and a new count
2963         # of how many times it has been renewed.
2964         my $renews = ( $issue->renewals || 0 ) + 1;
2965         my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2966                                 WHERE borrowernumber=?
2967                                 AND itemnumber=?"
2968         );
2969
2970         $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2971
2972         # Update the renewal count on the item, and tell zebra to reindex
2973         $renews = ( $item_object->renewals || 0 ) + 1;
2974         $item_object->renewals($renews);
2975         $item_object->onloan($datedue);
2976         $item_object->store({ log_action => 0 });
2977
2978         # Charge a new rental fee, if applicable
2979         my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2980         if ( $charge > 0 ) {
2981             AddIssuingCharge($issue, $charge, 'RENT_RENEW');
2982         }
2983
2984         # Charge a new accumulate rental fee, if applicable
2985         my $itemtype_object = Koha::ItemTypes->find( $itemtype );
2986         if ( $itemtype_object ) {
2987             my $accumulate_charge = $fees->accumulate_rentalcharge();
2988             if ( $accumulate_charge > 0 ) {
2989                 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY_RENEW' )
2990             }
2991             $charge += $accumulate_charge;
2992         }
2993
2994         # Send a renewal slip according to checkout alert preferencei
2995         if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2996             my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2997             my %conditions        = (
2998                 branchcode   => $branch,
2999                 categorycode => $patron->categorycode,
3000                 item_type    => $itemtype,
3001                 notification => 'CHECKOUT',
3002             );
3003             if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
3004                 SendCirculationAlert(
3005                     {
3006                         type     => 'RENEWAL',
3007                         item     => $item_unblessed,
3008                         borrower => $patron->unblessed,
3009                         branch   => $branch,
3010                     }
3011                 );
3012             }
3013         }
3014
3015         # Remove any OVERDUES related debarment if the borrower has no overdues
3016         if ( $patron
3017           && $patron->is_debarred
3018           && ! $patron->has_overdues
3019           && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
3020         ) {
3021             DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
3022         }
3023
3024         # Add the renewal to stats
3025         UpdateStats(
3026             {
3027                 branch         => $item_object->renewal_branchcode({branch => $branch}),
3028                 type           => 'renew',
3029                 amount         => $charge,
3030                 itemnumber     => $itemnumber,
3031                 itemtype       => $itemtype,
3032                 location       => $item_object->location,
3033                 borrowernumber => $borrowernumber,
3034                 ccode          => $item_object->ccode,
3035             }
3036         );
3037
3038         #Log the renewal
3039         logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
3040
3041         Koha::Plugins->call('after_circ_action', {
3042             action  => 'renewal',
3043             payload => {
3044                 checkout  => $issue->get_from_storage
3045             }
3046         });
3047     });
3048
3049     return $datedue;
3050 }
3051
3052 sub GetRenewCount {
3053     # check renewal status
3054     my ( $bornum, $itemno ) = @_;
3055     my $dbh           = C4::Context->dbh;
3056     my $renewcount    = 0;
3057     my $renewsallowed = 0;
3058     my $renewsleft    = 0;
3059
3060     my $patron = Koha::Patrons->find( $bornum );
3061     my $item   = Koha::Items->find($itemno);
3062
3063     return (0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
3064
3065     # Look in the issues table for this item, lent to this borrower,
3066     # and not yet returned.
3067
3068     # FIXME - I think this function could be redone to use only one SQL call.
3069     my $sth = $dbh->prepare(
3070         "select * from issues
3071                                 where (borrowernumber = ?)
3072                                 and (itemnumber = ?)"
3073     );
3074     $sth->execute( $bornum, $itemno );
3075     my $data = $sth->fetchrow_hashref;
3076     $renewcount = $data->{'renewals'} if $data->{'renewals'};
3077     # $item and $borrower should be calculated
3078     my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
3079
3080     my $rule = Koha::CirculationRules->get_effective_rule(
3081         {
3082             categorycode => $patron->categorycode,
3083             itemtype     => $item->effective_itemtype,
3084             branchcode   => $branchcode,
3085             rule_name    => 'renewalsallowed',
3086         }
3087     );
3088
3089     $renewsallowed = $rule ? $rule->rule_value : 0;
3090     $renewsleft    = $renewsallowed - $renewcount;
3091     if($renewsleft < 0){ $renewsleft = 0; }
3092     return ( $renewcount, $renewsallowed, $renewsleft );
3093 }
3094
3095 =head2 GetSoonestRenewDate
3096
3097   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3098
3099 Find out the soonest possible renew date of a borrowed item.
3100
3101 C<$borrowernumber> is the borrower number of the patron who currently
3102 has the item on loan.
3103
3104 C<$itemnumber> is the number of the item to renew.
3105
3106 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3107 renew date, based on the value "No renewal before" of the applicable
3108 issuing rule. Returns the current date if the item can already be
3109 renewed, and returns undefined if the borrower, loan, or item
3110 cannot be found.
3111
3112 =cut
3113
3114 sub GetSoonestRenewDate {
3115     my ( $borrowernumber, $itemnumber ) = @_;
3116
3117     my $dbh = C4::Context->dbh;
3118
3119     my $item      = Koha::Items->find($itemnumber)      or return;
3120     my $itemissue = $item->checkout or return;
3121
3122     $borrowernumber ||= $itemissue->borrowernumber;
3123     my $patron = Koha::Patrons->find( $borrowernumber )
3124       or return;
3125
3126     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3127     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
3128         {   categorycode => $patron->categorycode,
3129             itemtype     => $item->effective_itemtype,
3130             branchcode   => $branchcode,
3131             rules => [
3132                 'norenewalbefore',
3133                 'lengthunit',
3134             ]
3135         }
3136     );
3137
3138     my $now = dt_from_string;
3139     return $now unless $issuing_rule;
3140
3141     if ( defined $issuing_rule->{norenewalbefore}
3142         and $issuing_rule->{norenewalbefore} ne "" )
3143     {
3144         my $soonestrenewal =
3145           dt_from_string( $itemissue->date_due )->subtract(
3146             $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
3147
3148         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3149             and $issuing_rule->{lengthunit} eq 'days' )
3150         {
3151             $soonestrenewal->truncate( to => 'day' );
3152         }
3153         return $soonestrenewal if $now < $soonestrenewal;
3154     }
3155     return $now;
3156 }
3157
3158 =head2 GetLatestAutoRenewDate
3159
3160   $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3161
3162 Find out the latest possible auto renew date of a borrowed item.
3163
3164 C<$borrowernumber> is the borrower number of the patron who currently
3165 has the item on loan.
3166
3167 C<$itemnumber> is the number of the item to renew.
3168
3169 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3170 auto renew date, based on the value "No auto renewal after" and the "No auto
3171 renewal after (hard limit) of the applicable issuing rule.
3172 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3173 or item cannot be found.
3174
3175 =cut
3176
3177 sub GetLatestAutoRenewDate {
3178     my ( $borrowernumber, $itemnumber ) = @_;
3179
3180     my $dbh = C4::Context->dbh;
3181
3182     my $item      = Koha::Items->find($itemnumber)  or return;
3183     my $itemissue = $item->checkout                 or return;
3184
3185     $borrowernumber ||= $itemissue->borrowernumber;
3186     my $patron = Koha::Patrons->find( $borrowernumber )
3187       or return;
3188
3189     my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3190     my $circulation_rules = Koha::CirculationRules->get_effective_rules(
3191         {
3192             categorycode => $patron->categorycode,
3193             itemtype     => $item->effective_itemtype,
3194             branchcode   => $branchcode,
3195             rules => [
3196                 'no_auto_renewal_after',
3197                 'no_auto_renewal_after_hard_limit',
3198                 'lengthunit',
3199             ]
3200         }
3201     );
3202
3203     return unless $circulation_rules;
3204     return
3205       if ( not $circulation_rules->{no_auto_renewal_after}
3206             or $circulation_rules->{no_auto_renewal_after} eq '' )
3207       and ( not $circulation_rules->{no_auto_renewal_after_hard_limit}
3208              or $circulation_rules->{no_auto_renewal_after_hard_limit} eq '' );
3209
3210     my $maximum_renewal_date;
3211     if ( $circulation_rules->{no_auto_renewal_after} ) {
3212         $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3213         $maximum_renewal_date->add(
3214             $circulation_rules->{lengthunit} => $circulation_rules->{no_auto_renewal_after}
3215         );
3216     }
3217
3218     if ( $circulation_rules->{no_auto_renewal_after_hard_limit} ) {
3219         my $dt = dt_from_string( $circulation_rules->{no_auto_renewal_after_hard_limit} );
3220         $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3221     }
3222     return $maximum_renewal_date;
3223 }
3224
3225
3226 =head2 GetIssuingCharges
3227
3228   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3229
3230 Calculate how much it would cost for a given patron to borrow a given
3231 item, including any applicable discounts.
3232
3233 C<$itemnumber> is the item number of item the patron wishes to borrow.
3234
3235 C<$borrowernumber> is the patron's borrower number.
3236
3237 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3238 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3239 if it's a video).
3240
3241 =cut
3242
3243 sub GetIssuingCharges {
3244
3245     # calculate charges due
3246     my ( $itemnumber, $borrowernumber ) = @_;
3247     my $charge = 0;
3248     my $dbh    = C4::Context->dbh;
3249     my $item_type;
3250
3251     # Get the book's item type and rental charge (via its biblioitem).
3252     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3253         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3254     $charge_query .= (C4::Context->preference('item-level_itypes'))
3255         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3256         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3257
3258     $charge_query .= ' WHERE items.itemnumber =?';
3259
3260     my $sth = $dbh->prepare($charge_query);
3261     $sth->execute($itemnumber);
3262     if ( my $item_data = $sth->fetchrow_hashref ) {
3263         $item_type = $item_data->{itemtype};
3264         $charge    = $item_data->{rentalcharge};
3265         my $branch = C4::Context::mybranch();
3266         my $patron = Koha::Patrons->find( $borrowernumber );
3267         my $discount = _get_discount_from_rule($patron->categorycode, $branch, $item_type);
3268         if ($discount) {
3269             # We may have multiple rules so get the most specific
3270             $charge = ( $charge * ( 100 - $discount ) ) / 100;
3271         }
3272         if ($charge) {
3273             $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3274         }
3275     }
3276
3277     return ( $charge, $item_type );
3278 }
3279
3280 # Select most appropriate discount rule from those returned
3281 sub _get_discount_from_rule {
3282     my ($categorycode, $branchcode, $itemtype) = @_;
3283
3284     # Set search precedences
3285     my @params = (
3286         {
3287             branchcode   => $branchcode,
3288             itemtype     => $itemtype,
3289             categorycode => $categorycode,
3290         },
3291         {
3292             branchcode   => undef,
3293             categorycode => $categorycode,
3294             itemtype     => $itemtype,
3295         },
3296         {
3297             branchcode   => $branchcode,
3298             categorycode => $categorycode,
3299             itemtype     => undef,
3300         },
3301         {
3302             branchcode   => undef,
3303             categorycode => $categorycode,
3304             itemtype     => undef,
3305         },
3306     );
3307
3308     foreach my $params (@params) {
3309         my $rule = Koha::CirculationRules->search(
3310             {
3311                 rule_name => 'rentaldiscount',
3312                 %$params,
3313             }
3314         )->next();
3315
3316         return $rule->rule_value if $rule;
3317     }
3318
3319     # none of the above
3320     return 0;
3321 }
3322
3323 =head2 AddIssuingCharge
3324
3325   &AddIssuingCharge( $checkout, $charge, $type )
3326
3327 =cut
3328
3329 sub AddIssuingCharge {
3330     my ( $checkout, $charge, $type ) = @_;
3331
3332     # FIXME What if checkout does not exist?
3333
3334     my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3335     my $accountline = $account->add_debit(
3336         {
3337             amount      => $charge,
3338             note        => undef,
3339             user_id     => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
3340             library_id  => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3341             interface   => C4::Context->interface,
3342             type        => $type,
3343             item_id     => $checkout->itemnumber,
3344             issue_id    => $checkout->issue_id,
3345         }
3346     );
3347 }
3348
3349 =head2 GetTransfers
3350
3351   GetTransfers($itemnumber);
3352
3353 =cut
3354
3355 sub GetTransfers {
3356     my ($itemnumber) = @_;
3357
3358     my $dbh = C4::Context->dbh;
3359
3360     my $query = '
3361         SELECT datesent,
3362                frombranch,
3363                tobranch,
3364                branchtransfer_id
3365         FROM branchtransfers
3366         WHERE itemnumber = ?
3367           AND datearrived IS NULL
3368         ';
3369     my $sth = $dbh->prepare($query);
3370     $sth->execute($itemnumber);
3371     my @row = $sth->fetchrow_array();
3372     return @row;
3373 }
3374
3375 =head2 GetTransfersFromTo
3376
3377   @results = GetTransfersFromTo($frombranch,$tobranch);
3378
3379 Returns the list of pending transfers between $from and $to branch
3380
3381 =cut
3382
3383 sub GetTransfersFromTo {
3384     my ( $frombranch, $tobranch ) = @_;
3385     return unless ( $frombranch && $tobranch );
3386     my $dbh   = C4::Context->dbh;
3387     my $query = "
3388         SELECT branchtransfer_id,itemnumber,datesent,frombranch
3389         FROM   branchtransfers
3390         WHERE  frombranch=?
3391           AND  tobranch=?
3392           AND datearrived IS NULL
3393     ";
3394     my $sth = $dbh->prepare($query);
3395     $sth->execute( $frombranch, $tobranch );
3396     my @gettransfers;
3397
3398     while ( my $data = $sth->fetchrow_hashref ) {
3399         push @gettransfers, $data;
3400     }
3401     return (@gettransfers);
3402 }
3403
3404 =head2 DeleteTransfer
3405
3406   &DeleteTransfer($itemnumber);
3407
3408 =cut
3409
3410 sub DeleteTransfer {
3411     my ($itemnumber) = @_;
3412     return unless $itemnumber;
3413     my $dbh          = C4::Context->dbh;
3414     my $sth          = $dbh->prepare(
3415         "DELETE FROM branchtransfers
3416          WHERE itemnumber=?
3417          AND datearrived IS NULL "
3418     );
3419     return $sth->execute($itemnumber);
3420 }
3421
3422 =head2 SendCirculationAlert
3423
3424 Send out a C<check-in> or C<checkout> alert using the messaging system.
3425
3426 B<Parameters>:
3427
3428 =over 4
3429
3430 =item type
3431
3432 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3433
3434 =item item
3435
3436 Hashref of information about the item being checked in or out.
3437
3438 =item borrower
3439
3440 Hashref of information about the borrower of the item.
3441
3442 =item branch
3443
3444 The branchcode from where the checkout or check-in took place.
3445
3446 =back
3447
3448 B<Example>:
3449
3450     SendCirculationAlert({
3451         type     => 'CHECKOUT',
3452         item     => $item,
3453         borrower => $borrower,
3454         branch   => $branch,
3455     });
3456
3457 =cut
3458
3459 sub SendCirculationAlert {
3460     my ($opts) = @_;
3461     my ($type, $item, $borrower, $branch) =
3462         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3463     my %message_name = (
3464         CHECKIN  => 'Item_Check_in',
3465         CHECKOUT => 'Item_Checkout',
3466         RENEWAL  => 'Item_Checkout',
3467     );
3468     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3469         borrowernumber => $borrower->{borrowernumber},
3470         message_name   => $message_name{$type},
3471     });
3472     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3473
3474     my $schema = Koha::Database->new->schema;
3475     my @transports = keys %{ $borrower_preferences->{transports} };
3476
3477     # From the MySQL doc:
3478     # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3479     # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3480     # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3481     my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_TESTING};
3482
3483     for my $mtt (@transports) {
3484         my $letter =  C4::Letters::GetPreparedLetter (
3485             module => 'circulation',
3486             letter_code => $type,
3487             branchcode => $branch,
3488             message_transport_type => $mtt,
3489             lang => $borrower->{lang},
3490             tables => {
3491                 $issues_table => $item->{itemnumber},
3492                 'items'       => $item->{itemnumber},
3493                 'biblio'      => $item->{biblionumber},
3494                 'biblioitems' => $item->{biblionumber},
3495                 'borrowers'   => $borrower,
3496                 'branches'    => $branch,
3497             }
3498         ) or next;
3499
3500         $schema->storage->txn_begin;
3501         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3502         C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3503         my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3504         unless ( $message ) {
3505             C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3506             C4::Message->enqueue($letter, $borrower, $mtt);
3507         } else {
3508             $message->append($letter);
3509             $message->update;
3510         }
3511         C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3512         $schema->storage->txn_commit;
3513     }
3514
3515     return;
3516 }
3517
3518 =head2 updateWrongTransfer
3519
3520   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3521
3522 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 
3523
3524 =cut
3525
3526 sub updateWrongTransfer {
3527         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3528         my $dbh = C4::Context->dbh;     
3529 # first step validate the actual line of transfert .
3530         my $sth =
3531                 $dbh->prepare(
3532                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3533                 );
3534                 $sth->execute($FromLibrary,$itemNumber);
3535
3536 # second step create a new line of branchtransfer to the right location .
3537         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3538
3539 #third step changing holdingbranch of item
3540     my $item = Koha::Items->find($itemNumber)->holdingbranch($FromLibrary)->store;
3541 }
3542
3543 =head2 CalcDateDue
3544
3545 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3546
3547 this function calculates the due date given the start date and configured circulation rules,
3548 checking against the holidays calendar as per the daysmode circulation rule.
3549 C<$startdate>   = DateTime object representing start date of loan period (assumed to be today)
3550 C<$itemtype>  = itemtype code of item in question
3551 C<$branch>  = location whose calendar to use
3552 C<$borrower> = Borrower object
3553 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3554
3555 =cut
3556
3557 sub CalcDateDue {
3558     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3559
3560     $isrenewal ||= 0;
3561
3562     # loanlength now a href
3563     my $loanlength =
3564             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3565
3566     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3567             ? qq{renewalperiod}
3568             : qq{issuelength};
3569
3570     my $datedue;
3571     if ( $startdate ) {
3572         if (ref $startdate ne 'DateTime' ) {
3573             $datedue = dt_from_string($datedue);
3574         } else {
3575             $datedue = $startdate->clone;
3576         }
3577     } else {
3578         $datedue = dt_from_string()->truncate( to => 'minute' );
3579     }
3580
3581
3582     my $daysmode = Koha::CirculationRules->get_effective_daysmode(
3583         {
3584             categorycode => $borrower->{categorycode},
3585             itemtype     => $itemtype,
3586             branchcode   => $branch,
3587         }
3588     );
3589
3590     # calculate the datedue as normal
3591     if ( $daysmode eq 'Days' )
3592     {    # ignoring calendar
3593         if ( $loanlength->{lengthunit} eq 'hours' ) {
3594             $datedue->add( hours => $loanlength->{$length_key} );
3595         } else {    # days
3596             $datedue->add( days => $loanlength->{$length_key} );
3597             $datedue->set_hour(23);
3598             $datedue->set_minute(59);
3599         }
3600     } else {
3601         my $dur;
3602         if ($loanlength->{lengthunit} eq 'hours') {
3603             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3604         }
3605         else { # days
3606             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3607         }
3608         my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3609         $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3610         if ($loanlength->{lengthunit} eq 'days') {
3611             $datedue->set_hour(23);
3612             $datedue->set_minute(59);
3613         }
3614     }
3615
3616     # if Hard Due Dates are used, retrieve them and apply as necessary
3617     my ( $hardduedate, $hardduedatecompare ) =
3618       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3619     if ($hardduedate) {    # hardduedates are currently dates
3620         $hardduedate->truncate( to => 'minute' );
3621         $hardduedate->set_hour(23);
3622         $hardduedate->set_minute(59);
3623         my $cmp = DateTime->compare( $hardduedate, $datedue );
3624
3625 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3626 # if the calculated date is before the 'after' Hard Due Date (floor), override
3627 # if the hard due date is set to 'exactly', overrride
3628         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3629             $datedue = $hardduedate->clone;
3630         }
3631
3632         # in all other cases, keep the date due as it is
3633
3634     }
3635
3636     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3637     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3638         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3639         if( $expiry_dt ) { #skip empty expiry date..
3640             $expiry_dt->set( hour => 23, minute => 59);
3641             my $d1= $datedue->clone->set_time_zone('floating');
3642             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3643                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3644             }
3645         }
3646         if ( $daysmode ne 'Days' ) {
3647           my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3648           if ( $calendar->is_holiday($datedue) ) {
3649               # Don't return on a closed day
3650               $datedue = $calendar->prev_open_days( $datedue, 1 );
3651           }
3652         }
3653     }
3654
3655     return $datedue;
3656 }
3657
3658
3659 sub CheckValidBarcode{
3660 my ($barcode) = @_;
3661 my $dbh = C4::Context->dbh;
3662 my $query=qq|SELECT count(*) 
3663              FROM items 
3664              WHERE barcode=?
3665             |;
3666 my $sth = $dbh->prepare($query);
3667 $sth->execute($barcode);
3668 my $exist=$sth->fetchrow ;
3669 return $exist;
3670 }
3671
3672 =head2 IsBranchTransferAllowed
3673
3674   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3675
3676 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3677
3678 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3679 Koha::Item->can_be_transferred.
3680
3681 =cut
3682
3683 sub IsBranchTransferAllowed {
3684         my ( $toBranch, $fromBranch, $code ) = @_;
3685
3686         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3687         
3688         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3689         my $dbh = C4::Context->dbh;
3690             
3691         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3692         $sth->execute( $toBranch, $fromBranch, $code );
3693         my $limit = $sth->fetchrow_hashref();
3694                         
3695         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3696         if ( $limit->{'limitId'} ) {
3697                 return 0;
3698         } else {
3699                 return 1;
3700         }
3701 }                                                        
3702
3703 =head2 CreateBranchTransferLimit
3704
3705   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3706
3707 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3708
3709 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3710
3711 =cut
3712
3713 sub CreateBranchTransferLimit {
3714    my ( $toBranch, $fromBranch, $code ) = @_;
3715    return unless defined($toBranch) && defined($fromBranch);
3716    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3717    
3718    my $dbh = C4::Context->dbh;
3719    
3720    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3721    return $sth->execute( $code, $toBranch, $fromBranch );
3722 }
3723
3724 =head2 DeleteBranchTransferLimits
3725
3726     my $result = DeleteBranchTransferLimits($frombranch);
3727
3728 Deletes all the library transfer limits for one library.  Returns the
3729 number of limits deleted, 0e0 if no limits were deleted, or undef if
3730 no arguments are supplied.
3731
3732 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3733     fromBranch => $fromBranch
3734     })->delete.
3735
3736 =cut
3737
3738 sub DeleteBranchTransferLimits {
3739     my $branch = shift;
3740     return unless defined $branch;
3741     my $dbh    = C4::Context->dbh;
3742     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3743     return $sth->execute($branch);
3744 }
3745
3746 sub ReturnLostItem{
3747     my ( $borrowernumber, $itemnum ) = @_;
3748     MarkIssueReturned( $borrowernumber, $itemnum );
3749 }
3750
3751 =head2 LostItem
3752
3753   LostItem( $itemnumber, $mark_lost_from, $force_mark_returned, [$params] );
3754
3755 The final optional parameter, C<$params>, expected to contain
3756 'skip_record_index' key, which relayed down to Koha::Item/store,
3757 there it prevents calling of ModZebra index_records,
3758 which takes most of the time in batch adds/deletes: index_records better
3759 to be called later in C<additem.pl> after the whole loop.
3760
3761 $params:
3762     skip_record_index => 1|0
3763
3764 =cut
3765
3766 sub LostItem{
3767     my ($itemnumber, $mark_lost_from, $force_mark_returned, $params) = @_;
3768
3769     unless ( $mark_lost_from ) {
3770         # Temporary check to avoid regressions
3771         die q|LostItem called without $mark_lost_from, check the API.|;
3772     }
3773
3774     my $mark_returned;
3775     if ( $force_mark_returned ) {
3776         $mark_returned = 1;
3777     } else {
3778         my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3779         $mark_returned = ( $pref =~ m|$mark_lost_from| );
3780     }
3781
3782     my $dbh = C4::Context->dbh();
3783     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3784                            FROM issues 
3785                            JOIN items USING (itemnumber) 
3786                            JOIN biblio USING (biblionumber)
3787                            WHERE issues.itemnumber=?");
3788     $sth->execute($itemnumber);
3789     my $issues=$sth->fetchrow_hashref();
3790
3791     # If a borrower lost the item, add a replacement cost to the their record
3792     if ( my $borrowernumber = $issues->{borrowernumber} ){
3793         my $patron = Koha::Patrons->find( $borrowernumber );
3794
3795         my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 'LOST');
3796         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3797
3798         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3799             C4::Accounts::chargelostitem(
3800                 $borrowernumber,
3801                 $itemnumber,
3802                 $issues->{'replacementprice'},
3803                 sprintf( "%s %s %s",
3804                     $issues->{'title'}          || q{},
3805                     $issues->{'barcode'}        || q{},
3806                     $issues->{'itemcallnumber'} || q{},
3807                 ),
3808             );
3809             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3810             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3811         }
3812
3813         MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned;
3814     }
3815
3816     #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch)
3817     if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) {
3818         Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ skip_record_index => $params->{skip_record_index} });
3819     }
3820     my $transferdeleted = DeleteTransfer($itemnumber);
3821 }
3822
3823 sub GetOfflineOperations {
3824     my $dbh = C4::Context->dbh;
3825     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3826     $sth->execute(C4::Context->userenv->{'branch'});
3827     my $results = $sth->fetchall_arrayref({});
3828     return $results;
3829 }
3830
3831 sub GetOfflineOperation {
3832     my $operationid = shift;
3833     return unless $operationid;
3834     my $dbh = C4::Context->dbh;
3835     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3836     $sth->execute( $operationid );
3837     return $sth->fetchrow_hashref;
3838 }
3839
3840 sub AddOfflineOperation {
3841     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3842     my $dbh = C4::Context->dbh;
3843     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3844     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3845     return "Added.";
3846 }
3847
3848 sub DeleteOfflineOperation {
3849     my $dbh = C4::Context->dbh;
3850     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3851     $sth->execute( shift );
3852     return "Deleted.";
3853 }
3854
3855 sub ProcessOfflineOperation {
3856     my $operation = shift;
3857
3858     my $report;
3859     if ( $operation->{action} eq 'return' ) {
3860         $report = ProcessOfflineReturn( $operation );
3861     } elsif ( $operation->{action} eq 'issue' ) {
3862         $report = ProcessOfflineIssue( $operation );
3863     } elsif ( $operation->{action} eq 'payment' ) {
3864         $report = ProcessOfflinePayment( $operation );
3865     }
3866
3867     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3868
3869     return $report;
3870 }
3871
3872 sub ProcessOfflineReturn {
3873     my $operation = shift;
3874
3875     my $item = Koha::Items->find({barcode => $operation->{barcode}});
3876
3877     if ( $item ) {
3878         my $itemnumber = $item->itemnumber;
3879         my $issue = GetOpenIssue( $itemnumber );
3880         if ( $issue ) {
3881             my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
3882             ModDateLastSeen( $itemnumber, $leave_item_lost );
3883             MarkIssueReturned(
3884                 $issue->{borrowernumber},
3885                 $itemnumber,
3886                 $operation->{timestamp},
3887             );
3888             $item->renewals(0);
3889             $item->onloan(undef);
3890             $item->store({ log_action => 0 });
3891             return "Success.";
3892         } else {
3893             return "Item not issued.";
3894         }
3895     } else {
3896         return "Item not found.";
3897     }
3898 }
3899
3900 sub ProcessOfflineIssue {
3901     my $operation = shift;
3902
3903     my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3904
3905     if ( $patron ) {
3906         my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3907         unless ($item) {
3908             return "Barcode not found.";
3909         }
3910         my $itemnumber = $item->itemnumber;
3911         my $issue = GetOpenIssue( $itemnumber );
3912
3913         if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3914             MarkIssueReturned(
3915                 $issue->{borrowernumber},
3916                 $itemnumber,
3917                 $operation->{timestamp},
3918             );
3919         }
3920         AddIssue(
3921             $patron->unblessed,
3922             $operation->{'barcode'},
3923             undef,
3924             1,
3925             $operation->{timestamp},
3926             undef,
3927         );
3928         return "Success.";
3929     } else {
3930         return "Borrower not found.";
3931     }
3932 }
3933
3934 sub ProcessOfflinePayment {
3935     my $operation = shift;
3936
3937     my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
3938
3939     $patron->account->pay(
3940         {
3941             amount     => $operation->{amount},
3942             library_id => $operation->{branchcode},
3943             interface  => 'koc'
3944         }
3945     );
3946
3947     return "Success.";
3948 }
3949
3950 =head2 TransferSlip
3951
3952   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3953
3954   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3955
3956 =cut
3957
3958 sub TransferSlip {
3959     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3960
3961     my $item =
3962       $itemnumber
3963       ? Koha::Items->find($itemnumber)
3964       : Koha::Items->find( { barcode => $barcode } );
3965
3966     $item or return;
3967
3968     return C4::Letters::GetPreparedLetter (
3969         module => 'circulation',
3970         letter_code => 'TRANSFERSLIP',
3971         branchcode => $branch,
3972         tables => {
3973             'branches'    => $to_branch,
3974             'biblio'      => $item->biblionumber,
3975             'items'       => $item->unblessed,
3976         },
3977     );
3978 }
3979
3980 =head2 CheckIfIssuedToPatron
3981
3982   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3983
3984   Return 1 if any record item is issued to patron, otherwise return 0
3985
3986 =cut
3987
3988 sub CheckIfIssuedToPatron {
3989     my ($borrowernumber, $biblionumber) = @_;
3990
3991     my $dbh = C4::Context->dbh;
3992     my $query = q|
3993         SELECT COUNT(*) FROM issues
3994         LEFT JOIN items ON items.itemnumber = issues.itemnumber
3995         WHERE items.biblionumber = ?
3996         AND issues.borrowernumber = ?
3997     |;
3998     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3999     return 1 if $is_issued;
4000     return;
4001 }
4002
4003 =head2 IsItemIssued
4004
4005   IsItemIssued( $itemnumber )
4006
4007   Return 1 if the item is on loan, otherwise return 0
4008
4009 =cut
4010
4011 sub IsItemIssued {
4012     my $itemnumber = shift;
4013     my $dbh = C4::Context->dbh;
4014     my $sth = $dbh->prepare(q{
4015         SELECT COUNT(*)
4016         FROM issues
4017         WHERE itemnumber = ?
4018     });
4019     $sth->execute($itemnumber);
4020     return $sth->fetchrow;
4021 }
4022
4023 =head2 GetAgeRestriction
4024
4025   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
4026   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
4027
4028   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
4029   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
4030
4031 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
4032 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
4033 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
4034          Negative days mean the borrower has gone past the age restriction age.
4035
4036 =cut
4037
4038 sub GetAgeRestriction {
4039     my ($record_restrictions, $borrower) = @_;
4040     my $markers = C4::Context->preference('AgeRestrictionMarker');
4041
4042     return unless $record_restrictions;
4043     # Split $record_restrictions to something like FSK 16 or PEGI 6
4044     my @values = split ' ', uc($record_restrictions);
4045     return unless @values;
4046
4047     # Search first occurrence of one of the markers
4048     my @markers = split /\|/, uc($markers);
4049     return unless @markers;
4050
4051     my $index            = 0;
4052     my $restriction_year = 0;
4053     for my $value (@values) {
4054         $index++;
4055         for my $marker (@markers) {
4056             $marker =~ s/^\s+//;    #remove leading spaces
4057             $marker =~ s/\s+$//;    #remove trailing spaces
4058             if ( $marker eq $value ) {
4059                 if ( $index <= $#values ) {
4060                     $restriction_year += $values[$index];
4061                 }
4062                 last;
4063             }
4064             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
4065
4066                 # Perhaps it is something like "K16" (as in Finland)
4067                 $restriction_year += $1;
4068                 last;
4069             }
4070         }
4071         last if ( $restriction_year > 0 );
4072     }
4073
4074     #Check if the borrower is age restricted for this material and for how long.
4075     if ($restriction_year && $borrower) {
4076         if ( $borrower->{'dateofbirth'} ) {
4077             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
4078             $alloweddate[0] += $restriction_year;
4079
4080             #Prevent runime eror on leap year (invalid date)
4081             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
4082                 $alloweddate[2] = 28;
4083             }
4084
4085             #Get how many days the borrower has to reach the age restriction
4086             my @Today = split /-/, dt_from_string()->ymd();
4087             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
4088             #Negative days means the borrower went past the age restriction age
4089             return ($restriction_year, $daysToAgeRestriction);
4090         }
4091     }
4092
4093     return ($restriction_year);
4094 }
4095
4096
4097 =head2 GetPendingOnSiteCheckouts
4098
4099 =cut
4100
4101 sub GetPendingOnSiteCheckouts {
4102     my $dbh = C4::Context->dbh;
4103     return $dbh->selectall_arrayref(q|
4104         SELECT
4105           items.barcode,
4106           items.biblionumber,
4107           items.itemnumber,
4108           items.itemnotes,
4109           items.itemcallnumber,
4110           items.location,
4111           issues.date_due,
4112           issues.branchcode,
4113           issues.date_due < NOW() AS is_overdue,
4114           biblio.author,
4115           biblio.title,
4116           borrowers.firstname,
4117           borrowers.surname,
4118           borrowers.cardnumber,
4119           borrowers.borrowernumber
4120         FROM items
4121         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4122         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4123         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4124         WHERE issues.onsite_checkout = 1
4125     |, { Slice => {} } );
4126 }
4127
4128 sub GetTopIssues {
4129     my ($params) = @_;
4130
4131     my ($count, $branch, $itemtype, $ccode, $newness)
4132         = @$params{qw(count branch itemtype ccode newness)};
4133
4134     my $dbh = C4::Context->dbh;
4135     my $query = q{
4136         SELECT * FROM (
4137         SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4138           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4139           i.ccode, SUM(i.issues) AS count
4140         FROM biblio b
4141         LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4142         LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4143     };
4144
4145     my (@where_strs, @where_args);
4146
4147     if ($branch) {
4148         push @where_strs, 'i.homebranch = ?';
4149         push @where_args, $branch;
4150     }
4151     if ($itemtype) {
4152         if (C4::Context->preference('item-level_itypes')){
4153             push @where_strs, 'i.itype = ?';
4154             push @where_args, $itemtype;
4155         } else {
4156             push @where_strs, 'bi.itemtype = ?';
4157             push @where_args, $itemtype;
4158         }
4159     }
4160     if ($ccode) {
4161         push @where_strs, 'i.ccode = ?';
4162         push @where_args, $ccode;
4163     }
4164     if ($newness) {
4165         push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4166         push @where_args, $newness;
4167     }
4168
4169     if (@where_strs) {
4170         $query .= 'WHERE ' . join(' AND ', @where_strs);
4171     }
4172
4173     $query .= q{
4174         GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4175           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4176           i.ccode
4177         ORDER BY count DESC
4178     };
4179
4180     $query .= q{ ) xxx WHERE count > 0 };
4181     $count = int($count);
4182     if ($count > 0) {
4183         $query .= "LIMIT $count";
4184     }
4185
4186     my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4187
4188     return @$rows;
4189 }
4190
4191 =head2 Internal methods
4192
4193 =cut
4194
4195 sub _CalculateAndUpdateFine {
4196     my ($params) = @_;
4197
4198     my $borrower    = $params->{borrower};
4199     my $item        = $params->{item};
4200     my $issue       = $params->{issue};
4201     my $return_date = $params->{return_date};
4202
4203     unless ($borrower) { carp "No borrower passed in!" && return; }
4204     unless ($item)     { carp "No item passed in!"     && return; }
4205     unless ($issue)    { carp "No issue passed in!"    && return; }
4206
4207     my $datedue = dt_from_string( $issue->date_due );
4208
4209     # we only need to calculate and change the fines if we want to do that on return
4210     # Should be on for hourly loans
4211     my $control = C4::Context->preference('CircControl');
4212     my $control_branchcode =
4213         ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4214       : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
4215       :                                     $issue->branchcode;
4216
4217     my $date_returned = $return_date ? $return_date : dt_from_string();
4218
4219     my ( $amount, $unitcounttotal, $unitcount  ) =
4220       C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4221
4222     if ( C4::Context->preference('finesMode') eq 'production' ) {
4223         if ( $amount > 0 ) {
4224             C4::Overdues::UpdateFine({
4225                 issue_id       => $issue->issue_id,
4226                 itemnumber     => $issue->itemnumber,
4227                 borrowernumber => $issue->borrowernumber,
4228                 amount         => $amount,
4229                 due            => output_pref($datedue),
4230             });
4231         }
4232         elsif ($return_date) {
4233
4234             # Backdated returns may have fines that shouldn't exist,
4235             # so in this case, we need to drop those fines to 0
4236
4237             C4::Overdues::UpdateFine({
4238                 issue_id       => $issue->issue_id,
4239                 itemnumber     => $issue->itemnumber,
4240                 borrowernumber => $issue->borrowernumber,
4241                 amount         => 0,
4242                 due            => output_pref($datedue),
4243             });
4244         }
4245     }
4246 }
4247
4248 sub _item_denied_renewal {
4249     my ($params) = @_;
4250
4251     my $item = $params->{item};
4252     return unless $item;
4253
4254     my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4255     return unless $denyingrules;
4256     foreach my $field (keys %$denyingrules) {
4257         my $val = $item->$field;
4258         if( !defined $val) {
4259             if ( any { !defined $_ }  @{$denyingrules->{$field}} ){
4260                 return 1;
4261             }
4262         } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4263            # If the results matches the values in the syspref
4264            # We return true if match found
4265             return 1;
4266         }
4267     }
4268     return 0;
4269 }
4270
4271 1;
4272
4273 __END__
4274
4275 =head1 AUTHOR
4276
4277 Koha Development Team <http://koha-community.org/>
4278
4279 =cut