Bug 10859: Alert if a borrower already has an issue for the same biblio
[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 under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
11 # version.
12 #
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with Koha; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use DateTime;
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::Dates;
32 use C4::Dates qw(format_date);
33 use C4::Accounts;
34 use C4::ItemCirculationAlertPreference;
35 use C4::Message;
36 use C4::Debug;
37 use C4::Branch; # GetBranches
38 use C4::Log; # logaction
39 use C4::Koha qw(
40     GetAuthorisedValueByCode
41     GetAuthValCode
42     GetKohaAuthorisedValueLib
43 );
44 use C4::Overdues qw(CalcFine UpdateFine);
45 use Algorithm::CheckDigits;
46
47 use Data::Dumper;
48 use Koha::DateUtils;
49 use Koha::Calendar;
50 use Koha::Borrower::Debarments;
51 use Carp;
52 use Date::Calc qw(
53   Today
54   Today_and_Now
55   Add_Delta_YM
56   Add_Delta_DHMS
57   Date_to_Days
58   Day_of_Week
59   Add_Delta_Days
60 );
61 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
62
63 BEGIN {
64         require Exporter;
65     $VERSION = 3.07.00.049;     # for version checking
66         @ISA    = qw(Exporter);
67
68         # FIXME subs that should probably be elsewhere
69         push @EXPORT, qw(
70                 &barcodedecode
71         &LostItem
72         &ReturnLostItem
73         );
74
75         # subs to deal with issuing a book
76         push @EXPORT, qw(
77                 &CanBookBeIssued
78                 &CanBookBeRenewed
79                 &AddIssue
80                 &AddRenewal
81                 &GetRenewCount
82         &GetSoonestRenewDate
83                 &GetItemIssue
84                 &GetItemIssues
85                 &GetIssuingCharges
86                 &GetIssuingRule
87         &GetBranchBorrowerCircRule
88         &GetBranchItemRule
89                 &GetBiblioIssues
90                 &GetOpenIssue
91                 &AnonymiseIssueHistory
92         &CheckIfIssuedToPatron
93         &IsItemIssued
94         );
95
96         # subs to deal with returns
97         push @EXPORT, qw(
98                 &AddReturn
99         &MarkIssueReturned
100         );
101
102         # subs to deal with transfers
103         push @EXPORT, qw(
104                 &transferbook
105                 &GetTransfers
106                 &GetTransfersFromTo
107                 &updateWrongTransfer
108                 &DeleteTransfer
109                 &IsBranchTransferAllowed
110                 &CreateBranchTransferLimit
111                 &DeleteBranchTransferLimits
112         &TransferSlip
113         );
114
115     # subs to deal with offline circulation
116     push @EXPORT, qw(
117       &GetOfflineOperations
118       &GetOfflineOperation
119       &AddOfflineOperation
120       &DeleteOfflineOperation
121       &ProcessOfflineOperation
122     );
123 }
124
125 =head1 NAME
126
127 C4::Circulation - Koha circulation module
128
129 =head1 SYNOPSIS
130
131 use C4::Circulation;
132
133 =head1 DESCRIPTION
134
135 The functions in this module deal with circulation, issues, and
136 returns, as well as general information about the library.
137 Also deals with stocktaking.
138
139 =head1 FUNCTIONS
140
141 =head2 barcodedecode
142
143   $str = &barcodedecode($barcode, [$filter]);
144
145 Generic filter function for barcode string.
146 Called on every circ if the System Pref itemBarcodeInputFilter is set.
147 Will do some manipulation of the barcode for systems that deliver a barcode
148 to circulation.pl that differs from the barcode stored for the item.
149 For proper functioning of this filter, calling the function on the 
150 correct barcode string (items.barcode) should return an unaltered barcode.
151
152 The optional $filter argument is to allow for testing or explicit 
153 behavior that ignores the System Pref.  Valid values are the same as the 
154 System Pref options.
155
156 =cut
157
158 # FIXME -- the &decode fcn below should be wrapped into this one.
159 # FIXME -- these plugins should be moved out of Circulation.pm
160 #
161 sub barcodedecode {
162     my ($barcode, $filter) = @_;
163     my $branch = C4::Branch::mybranch();
164     $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
165     $filter or return $barcode;     # ensure filter is defined, else return untouched barcode
166         if ($filter eq 'whitespace') {
167                 $barcode =~ s/\s//g;
168         } elsif ($filter eq 'cuecat') {
169                 chomp($barcode);
170             my @fields = split( /\./, $barcode );
171             my @results = map( decode($_), @fields[ 1 .. $#fields ] );
172             ($#results == 2) and return $results[2];
173         } elsif ($filter eq 'T-prefix') {
174                 if ($barcode =~ /^[Tt](\d)/) {
175                         (defined($1) and $1 eq '0') and return $barcode;
176             $barcode = substr($barcode, 2) + 0;     # FIXME: probably should be substr($barcode, 1)
177                 }
178         return sprintf("T%07d", $barcode);
179         # FIXME: $barcode could be "T1", causing warning: substr outside of string
180         # Why drop the nonzero digit after the T?
181         # Why pass non-digits (or empty string) to "T%07d"?
182         } elsif ($filter eq 'libsuite8') {
183                 unless($barcode =~ m/^($branch)-/i){    #if barcode starts with branch code its in Koha style. Skip it.
184                         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
185                                 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
186                         }else{
187                                 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
188                         }
189                 }
190     } elsif ($filter eq 'EAN13') {
191         my $ean = CheckDigits('ean');
192         if ( $ean->is_valid($barcode) ) {
193             #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
194             $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
195         } else {
196             warn "# [$barcode] not valid EAN-13/UPC-A\n";
197         }
198         }
199     return $barcode;    # return barcode, modified or not
200 }
201
202 =head2 decode
203
204   $str = &decode($chunk);
205
206 Decodes a segment of a string emitted by a CueCat barcode scanner and
207 returns it.
208
209 FIXME: Should be replaced with Barcode::Cuecat from CPAN
210 or Javascript based decoding on the client side.
211
212 =cut
213
214 sub decode {
215     my ($encoded) = @_;
216     my $seq =
217       'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
218     my @s = map { index( $seq, $_ ); } split( //, $encoded );
219     my $l = ( $#s + 1 ) % 4;
220     if ($l) {
221         if ( $l == 1 ) {
222             # warn "Error: Cuecat decode parsing failed!";
223             return;
224         }
225         $l = 4 - $l;
226         $#s += $l;
227     }
228     my $r = '';
229     while ( $#s >= 0 ) {
230         my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
231         $r .=
232             chr( ( $n >> 16 ) ^ 67 )
233          .chr( ( $n >> 8 & 255 ) ^ 67 )
234          .chr( ( $n & 255 ) ^ 67 );
235         @s = @s[ 4 .. $#s ];
236     }
237     $r = substr( $r, 0, length($r) - $l );
238     return $r;
239 }
240
241 =head2 transferbook
242
243   ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch, 
244                                             $barcode, $ignore_reserves);
245
246 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
247
248 C<$newbranch> is the code for the branch to which the item should be transferred.
249
250 C<$barcode> is the barcode of the item to be transferred.
251
252 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
253 Otherwise, if an item is reserved, the transfer fails.
254
255 Returns three values:
256
257 =over
258
259 =item $dotransfer 
260
261 is true if the transfer was successful.
262
263 =item $messages
264
265 is a reference-to-hash which may have any of the following keys:
266
267 =over
268
269 =item C<BadBarcode>
270
271 There is no item in the catalog with the given barcode. The value is C<$barcode>.
272
273 =item C<IsPermanent>
274
275 The item's home branch is permanent. This doesn't prevent the item from being transferred, though. The value is the code of the item's home branch.
276
277 =item C<DestinationEqualsHolding>
278
279 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.
280
281 =item C<WasReturned>
282
283 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.
284
285 =item C<ResFound>
286
287 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>.
288
289 =item C<WasTransferred>
290
291 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
292
293 =back
294
295 =back
296
297 =cut
298
299 sub transferbook {
300     my ( $tbr, $barcode, $ignoreRs ) = @_;
301     my $messages;
302     my $dotransfer      = 1;
303     my $branches        = GetBranches();
304     my $itemnumber = GetItemnumberFromBarcode( $barcode );
305     my $issue      = GetItemIssue($itemnumber);
306     my $biblio = GetBiblioFromItemNumber($itemnumber);
307
308     # bad barcode..
309     if ( not $itemnumber ) {
310         $messages->{'BadBarcode'} = $barcode;
311         $dotransfer = 0;
312     }
313
314     # get branches of book...
315     my $hbr = $biblio->{'homebranch'};
316     my $fbr = $biblio->{'holdingbranch'};
317
318     # if using Branch Transfer Limits
319     if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
320         if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
321             if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
322                 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
323                 $dotransfer = 0;
324             }
325         } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
326             $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
327             $dotransfer = 0;
328         }
329     }
330
331     # if is permanent...
332     if ( $hbr && $branches->{$hbr}->{'PE'} ) {
333         $messages->{'IsPermanent'} = $hbr;
334         $dotransfer = 0;
335     }
336
337     # can't transfer book if is already there....
338     if ( $fbr eq $tbr ) {
339         $messages->{'DestinationEqualsHolding'} = 1;
340         $dotransfer = 0;
341     }
342
343     # check if it is still issued to someone, return it...
344     if ($issue->{borrowernumber}) {
345         AddReturn( $barcode, $fbr );
346         $messages->{'WasReturned'} = $issue->{borrowernumber};
347     }
348
349     # find reserves.....
350     # That'll save a database query.
351     my ( $resfound, $resrec, undef ) =
352       CheckReserves( $itemnumber );
353     if ( $resfound and not $ignoreRs ) {
354         $resrec->{'ResFound'} = $resfound;
355
356         #         $messages->{'ResFound'} = $resrec;
357         $dotransfer = 1;
358     }
359
360     #actually do the transfer....
361     if ($dotransfer) {
362         ModItemTransfer( $itemnumber, $fbr, $tbr );
363
364         # don't need to update MARC anymore, we do it in batch now
365         $messages->{'WasTransfered'} = 1;
366
367     }
368     ModDateLastSeen( $itemnumber );
369     return ( $dotransfer, $messages, $biblio );
370 }
371
372
373 sub TooMany {
374     my $borrower        = shift;
375     my $biblionumber = shift;
376         my $item                = shift;
377     my $cat_borrower    = $borrower->{'categorycode'};
378     my $dbh             = C4::Context->dbh;
379         my $branch;
380         # Get which branchcode we need
381         $branch = _GetCircControlBranch($item,$borrower);
382         my $type = (C4::Context->preference('item-level_itypes')) 
383                         ? $item->{'itype'}         # item-level
384                         : $item->{'itemtype'};     # biblio-level
385  
386     # given branch, patron category, and item type, determine
387     # applicable issuing rule
388     my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
389
390     # if a rule is found and has a loan limit set, count
391     # how many loans the patron already has that meet that
392     # rule
393     if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
394         my @bind_params;
395         my $count_query = "SELECT COUNT(*) FROM issues
396                            JOIN items USING (itemnumber) ";
397
398         my $rule_itemtype = $issuing_rule->{itemtype};
399         if ($rule_itemtype eq "*") {
400             # matching rule has the default item type, so count only
401             # those existing loans that don't fall under a more
402             # specific rule
403             if (C4::Context->preference('item-level_itypes')) {
404                 $count_query .= " WHERE items.itype NOT IN (
405                                     SELECT itemtype FROM issuingrules
406                                     WHERE branchcode = ?
407                                     AND   (categorycode = ? OR categorycode = ?)
408                                     AND   itemtype <> '*'
409                                   ) ";
410             } else { 
411                 $count_query .= " JOIN  biblioitems USING (biblionumber) 
412                                   WHERE biblioitems.itemtype NOT IN (
413                                     SELECT itemtype FROM issuingrules
414                                     WHERE branchcode = ?
415                                     AND   (categorycode = ? OR categorycode = ?)
416                                     AND   itemtype <> '*'
417                                   ) ";
418             }
419             push @bind_params, $issuing_rule->{branchcode};
420             push @bind_params, $issuing_rule->{categorycode};
421             push @bind_params, $cat_borrower;
422         } else {
423             # rule has specific item type, so count loans of that
424             # specific item type
425             if (C4::Context->preference('item-level_itypes')) {
426                 $count_query .= " WHERE items.itype = ? ";
427             } else { 
428                 $count_query .= " JOIN  biblioitems USING (biblionumber) 
429                                   WHERE biblioitems.itemtype= ? ";
430             }
431             push @bind_params, $type;
432         }
433
434         $count_query .= " AND borrowernumber = ? ";
435         push @bind_params, $borrower->{'borrowernumber'};
436         my $rule_branch = $issuing_rule->{branchcode};
437         if ($rule_branch ne "*") {
438             if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
439                 $count_query .= " AND issues.branchcode = ? ";
440                 push @bind_params, $branch;
441             } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
442                 ; # if branch is the patron's home branch, then count all loans by patron
443             } else {
444                 $count_query .= " AND items.homebranch = ? ";
445                 push @bind_params, $branch;
446             }
447         }
448
449         my $count_sth = $dbh->prepare($count_query);
450         $count_sth->execute(@bind_params);
451         my ($current_loan_count) = $count_sth->fetchrow_array;
452
453         my $max_loans_allowed = $issuing_rule->{'maxissueqty'};
454         if ($current_loan_count >= $max_loans_allowed) {
455             return ($current_loan_count, $max_loans_allowed);
456         }
457     }
458
459     # Now count total loans against the limit for the branch
460     my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
461     if (defined($branch_borrower_circ_rule->{maxissueqty})) {
462         my @bind_params = ();
463         my $branch_count_query = "SELECT COUNT(*) FROM issues
464                                   JOIN items USING (itemnumber)
465                                   WHERE borrowernumber = ? ";
466         push @bind_params, $borrower->{borrowernumber};
467
468         if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
469             $branch_count_query .= " AND issues.branchcode = ? ";
470             push @bind_params, $branch;
471         } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
472             ; # if branch is the patron's home branch, then count all loans by patron
473         } else {
474             $branch_count_query .= " AND items.homebranch = ? ";
475             push @bind_params, $branch;
476         }
477         my $branch_count_sth = $dbh->prepare($branch_count_query);
478         $branch_count_sth->execute(@bind_params);
479         my ($current_loan_count) = $branch_count_sth->fetchrow_array;
480
481         my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty};
482         if ($current_loan_count >= $max_loans_allowed) {
483             return ($current_loan_count, $max_loans_allowed);
484         }
485     }
486
487     # OK, the patron can issue !!!
488     return;
489 }
490
491 =head2 itemissues
492
493   @issues = &itemissues($biblioitemnumber, $biblio);
494
495 Looks up information about who has borrowed the bookZ<>(s) with the
496 given biblioitemnumber.
497
498 C<$biblio> is ignored.
499
500 C<&itemissues> returns an array of references-to-hash. The keys
501 include the fields from the C<items> table in the Koha database.
502 Additional keys include:
503
504 =over 4
505
506 =item C<date_due>
507
508 If the item is currently on loan, this gives the due date.
509
510 If the item is not on loan, then this is either "Available" or
511 "Cancelled", if the item has been withdrawn.
512
513 =item C<card>
514
515 If the item is currently on loan, this gives the card number of the
516 patron who currently has the item.
517
518 =item C<timestamp0>, C<timestamp1>, C<timestamp2>
519
520 These give the timestamp for the last three times the item was
521 borrowed.
522
523 =item C<card0>, C<card1>, C<card2>
524
525 The card number of the last three patrons who borrowed this item.
526
527 =item C<borrower0>, C<borrower1>, C<borrower2>
528
529 The borrower number of the last three patrons who borrowed this item.
530
531 =back
532
533 =cut
534
535 #'
536 sub itemissues {
537     my ( $bibitem, $biblio ) = @_;
538     my $dbh = C4::Context->dbh;
539     my $sth =
540       $dbh->prepare("Select * from items where items.biblioitemnumber = ?")
541       || die $dbh->errstr;
542     my $i = 0;
543     my @results;
544
545     $sth->execute($bibitem) || die $sth->errstr;
546
547     while ( my $data = $sth->fetchrow_hashref ) {
548
549         # Find out who currently has this item.
550         # FIXME - Wouldn't it be better to do this as a left join of
551         # some sort? Currently, this code assumes that if
552         # fetchrow_hashref() fails, then the book is on the shelf.
553         # fetchrow_hashref() can fail for any number of reasons (e.g.,
554         # database server crash), not just because no items match the
555         # search criteria.
556         my $sth2 = $dbh->prepare(
557             "SELECT * FROM issues
558                 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
559                 WHERE itemnumber = ?
560             "
561         );
562
563         $sth2->execute( $data->{'itemnumber'} );
564         if ( my $data2 = $sth2->fetchrow_hashref ) {
565             $data->{'date_due'} = $data2->{'date_due'};
566             $data->{'card'}     = $data2->{'cardnumber'};
567             $data->{'borrower'} = $data2->{'borrowernumber'};
568         }
569         else {
570             $data->{'date_due'} = ($data->{'withdrawn'} eq '1') ? 'Cancelled' : 'Available';
571         }
572
573
574         # Find the last 3 people who borrowed this item.
575         $sth2 = $dbh->prepare(
576             "SELECT * FROM old_issues
577                 LEFT JOIN borrowers ON  issues.borrowernumber = borrowers.borrowernumber
578                 WHERE itemnumber = ?
579                 ORDER BY returndate DESC,timestamp DESC"
580         );
581
582         $sth2->execute( $data->{'itemnumber'} );
583         for ( my $i2 = 0 ; $i2 < 2 ; $i2++ )
584         {    # FIXME : error if there is less than 3 pple borrowing this item
585             if ( my $data2 = $sth2->fetchrow_hashref ) {
586                 $data->{"timestamp$i2"} = $data2->{'timestamp'};
587                 $data->{"card$i2"}      = $data2->{'cardnumber'};
588                 $data->{"borrower$i2"}  = $data2->{'borrowernumber'};
589             }    # if
590         }    # for
591
592         $results[$i] = $data;
593         $i++;
594     }
595
596     return (@results);
597 }
598
599 =head2 CanBookBeIssued
600
601   ( $issuingimpossible, $needsconfirmation ) =  CanBookBeIssued( $borrower, 
602                       $barcode, $duedatespec, $inprocess, $ignore_reserves );
603
604 Check if a book can be issued.
605
606 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
607
608 =over 4
609
610 =item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
611
612 =item C<$barcode> is the bar code of the book being issued.
613
614 =item C<$duedatespec> is a C4::Dates object.
615
616 =item C<$inprocess> boolean switch
617 =item C<$ignore_reserves> boolean switch
618
619 =back
620
621 Returns :
622
623 =over 4
624
625 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
626 Possible values are :
627
628 =back
629
630 =head3 INVALID_DATE 
631
632 sticky due date is invalid
633
634 =head3 GNA
635
636 borrower gone with no address
637
638 =head3 CARD_LOST
639
640 borrower declared it's card lost
641
642 =head3 DEBARRED
643
644 borrower debarred
645
646 =head3 UNKNOWN_BARCODE
647
648 barcode unknown
649
650 =head3 NOT_FOR_LOAN
651
652 item is not for loan
653
654 =head3 WTHDRAWN
655
656 item withdrawn.
657
658 =head3 RESTRICTED
659
660 item is restricted (set by ??)
661
662 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan 
663 could be prevented, but ones that can be overriden by the operator.
664
665 Possible values are :
666
667 =head3 DEBT
668
669 borrower has debts.
670
671 =head3 RENEW_ISSUE
672
673 renewing, not issuing
674
675 =head3 ISSUED_TO_ANOTHER
676
677 issued to someone else.
678
679 =head3 RESERVED
680
681 reserved for someone else.
682
683 =head3 INVALID_DATE
684
685 sticky due date is invalid or due date in the past
686
687 =head3 TOO_MANY
688
689 if the borrower borrows to much things
690
691 =cut
692
693 sub CanBookBeIssued {
694     my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves ) = @_;
695     my %needsconfirmation;    # filled with problems that needs confirmations
696     my %issuingimpossible;    # filled with problems that causes the issue to be IMPOSSIBLE
697     my %alerts;               # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
698
699     my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
700     my $issue = GetItemIssue($item->{itemnumber});
701         my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
702         $item->{'itemtype'}=$item->{'itype'}; 
703     my $dbh             = C4::Context->dbh;
704
705     # MANDATORY CHECKS - unless item exists, nothing else matters
706     unless ( $item->{barcode} ) {
707         $issuingimpossible{UNKNOWN_BARCODE} = 1;
708     }
709         return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
710
711     #
712     # DUE DATE is OK ? -- should already have checked.
713     #
714     if ($duedate && ref $duedate ne 'DateTime') {
715         $duedate = dt_from_string($duedate);
716     }
717     my $now = DateTime->now( time_zone => C4::Context->tz() );
718     unless ( $duedate ) {
719         my $issuedate = $now->clone();
720
721         my $branch = _GetCircControlBranch($item,$borrower);
722         my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
723         $duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
724
725         # Offline circ calls AddIssue directly, doesn't run through here
726         #  So issuingimpossible should be ok.
727     }
728     if ($duedate) {
729         my $today = $now->clone();
730         $today->truncate( to => 'minute');
731         if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
732             $needsconfirmation{INVALID_DATE} = output_pref($duedate);
733         }
734     } else {
735             $issuingimpossible{INVALID_DATE} = output_pref($duedate);
736     }
737
738     #
739     # BORROWER STATUS
740     #
741     if ( $borrower->{'category_type'} eq 'X' && (  $item->{barcode}  )) { 
742         # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1  .
743         &UpdateStats(C4::Context->userenv->{'branch'},'localuse','','',$item->{'itemnumber'},$item->{'itemtype'},$borrower->{'borrowernumber'}, undef, $item->{'ccode'});
744         ModDateLastSeen( $item->{'itemnumber'} );
745         return( { STATS => 1 }, {});
746     }
747     if ( $borrower->{flags}->{GNA} ) {
748         $issuingimpossible{GNA} = 1;
749     }
750     if ( $borrower->{flags}->{'LOST'} ) {
751         $issuingimpossible{CARD_LOST} = 1;
752     }
753     if ( $borrower->{flags}->{'DBARRED'} ) {
754         $issuingimpossible{DEBARRED} = 1;
755     }
756     if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
757         $issuingimpossible{EXPIRED} = 1;
758     } else {
759         my ($y, $m, $d) =  split /-/,$borrower->{'dateexpiry'};
760         if ($y && $m && $d) { # are we really writing oinvalid dates to borrs
761             my $expiry_dt = DateTime->new(
762                 year => $y,
763                 month => $m,
764                 day   => $d,
765                 time_zone => C4::Context->tz,
766             );
767             $expiry_dt->truncate( to => 'day');
768             my $today = $now->clone()->truncate(to => 'day');
769             if (DateTime->compare($today, $expiry_dt) == 1) {
770                 $issuingimpossible{EXPIRED} = 1;
771             }
772         } else {
773             carp("Invalid expity date in borr");
774             $issuingimpossible{EXPIRED} = 1;
775         }
776     }
777     #
778     # BORROWER STATUS
779     #
780
781     # DEBTS
782     my ($balance, $non_issue_charges, $other_charges) =
783       C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} );
784     my $amountlimit = C4::Context->preference("noissuescharge");
785     my $allowfineoverride = C4::Context->preference("AllowFineOverride");
786     my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
787     if ( C4::Context->preference("IssuingInProcess") ) {
788         if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
789             $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
790         } elsif ( $non_issue_charges > $amountlimit && !$inprocess && $allowfineoverride) {
791             $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
792         } elsif ( $allfinesneedoverride && $non_issue_charges > 0 && $non_issue_charges <= $amountlimit && !$inprocess ) {
793             $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
794         }
795     }
796     else {
797         if ( $non_issue_charges > $amountlimit && $allowfineoverride ) {
798             $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
799         } elsif ( $non_issue_charges > $amountlimit && !$allowfineoverride) {
800             $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
801         } elsif ( $non_issue_charges > 0 && $allfinesneedoverride ) {
802             $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
803         }
804     }
805     if ($balance > 0 && $other_charges > 0) {
806         $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
807     }
808
809     my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'});
810     if ($blocktype == -1) {
811         ## patron has outstanding overdue loans
812             if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
813                 $issuingimpossible{USERBLOCKEDOVERDUE} = $count;
814             }
815             elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
816                 $needsconfirmation{USERBLOCKEDOVERDUE} = $count;
817             }
818     } elsif($blocktype == 1) {
819         # patron has accrued fine days
820         $issuingimpossible{USERBLOCKEDREMAINING} = $count;
821     }
822
823 #
824     # JB34 CHECKS IF BORROWERS DONT HAVE ISSUE TOO MANY BOOKS
825     #
826         my ($current_loan_count, $max_loans_allowed) = TooMany( $borrower, $item->{biblionumber}, $item );
827     # if TooMany max_loans_allowed returns 0 the user doesn't have permission to check out this book
828     if (defined $max_loans_allowed && $max_loans_allowed == 0) {
829         $needsconfirmation{PATRON_CANT} = 1;
830     } else {
831         if($max_loans_allowed){
832             if ( C4::Context->preference("AllowTooManyOverride") ) {
833                 $needsconfirmation{TOO_MANY} = 1;
834                 $needsconfirmation{current_loan_count} = $current_loan_count;
835                 $needsconfirmation{max_loans_allowed} = $max_loans_allowed;
836             } else {
837                 $issuingimpossible{TOO_MANY} = 1;
838                 $issuingimpossible{current_loan_count} = $current_loan_count;
839                 $issuingimpossible{max_loans_allowed} = $max_loans_allowed;
840             }
841         }
842     }
843
844     #
845     # ITEM CHECKING
846     #
847     if ( $item->{'notforloan'} )
848     {
849         if(!C4::Context->preference("AllowNotForLoanOverride")){
850             $issuingimpossible{NOT_FOR_LOAN} = 1;
851             $issuingimpossible{item_notforloan} = $item->{'notforloan'};
852         }else{
853             $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
854             $needsconfirmation{item_notforloan} = $item->{'notforloan'};
855         }
856     }
857     else {
858         # we have to check itemtypes.notforloan also
859         if (C4::Context->preference('item-level_itypes')){
860             # this should probably be a subroutine
861             my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
862             $sth->execute($item->{'itemtype'});
863             my $notforloan=$sth->fetchrow_hashref();
864             if ($notforloan->{'notforloan'}) {
865                 if (!C4::Context->preference("AllowNotForLoanOverride")) {
866                     $issuingimpossible{NOT_FOR_LOAN} = 1;
867                     $issuingimpossible{itemtype_notforloan} = $item->{'itype'};
868                 } else {
869                     $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
870                     $needsconfirmation{itemtype_notforloan} = $item->{'itype'};
871                 }
872             }
873         }
874         elsif ($biblioitem->{'notforloan'} == 1){
875             if (!C4::Context->preference("AllowNotForLoanOverride")) {
876                 $issuingimpossible{NOT_FOR_LOAN} = 1;
877                 $issuingimpossible{itemtype_notforloan} = $biblioitem->{'itemtype'};
878             } else {
879                 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
880                 $needsconfirmation{itemtype_notforloan} = $biblioitem->{'itemtype'};
881             }
882         }
883     }
884     if ( $item->{'withdrawn'} && $item->{'withdrawn'} > 0 )
885     {
886         $issuingimpossible{WTHDRAWN} = 1;
887     }
888     if (   $item->{'restricted'}
889         && $item->{'restricted'} == 1 )
890     {
891         $issuingimpossible{RESTRICTED} = 1;
892     }
893     if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
894         my $code = GetAuthorisedValueByCode( 'LOST', $item->{'itemlost'} );
895         $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
896         $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
897     }
898     if ( C4::Context->preference("IndependentBranches") ) {
899         my $userenv = C4::Context->userenv;
900         unless ( C4::Context->IsSuperLibrarian() ) {
901             if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} ){
902                 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
903                 $issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")};
904             }
905             $needsconfirmation{BORRNOTSAMEBRANCH} = GetBranchName( $borrower->{'branchcode'} )
906               if ( $borrower->{'branchcode'} ne $userenv->{branch} );
907         }
908     }
909
910     #
911     # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
912     #
913     if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} )
914     {
915
916         # Already issued to current borrower. Ask whether the loan should
917         # be renewed.
918         my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
919             $borrower->{'borrowernumber'},
920             $item->{'itemnumber'}
921         );
922         if ( $CanBookBeRenewed == 0 ) {    # no more renewals allowed
923             $issuingimpossible{NO_MORE_RENEWALS} = 1;
924         }
925         else {
926             $needsconfirmation{RENEW_ISSUE} = 1;
927         }
928     }
929     elsif ($issue->{borrowernumber}) {
930
931         # issued to someone else
932         my $currborinfo =    C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
933
934 #        warn "=>.$currborinfo->{'firstname'} $currborinfo->{'surname'} ($currborinfo->{'cardnumber'})";
935         $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
936         $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
937         $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
938         $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
939         $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
940     }
941
942     unless ( $ignore_reserves ) {
943         # See if the item is on reserve.
944         my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
945         if ($restype) {
946             my $resbor = $res->{'borrowernumber'};
947             if ( $resbor ne $borrower->{'borrowernumber'} ) {
948                 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
949                 my $branchname = GetBranchName( $res->{'branchcode'} );
950                 if ( $restype eq "Waiting" )
951                 {
952                     # The item is on reserve and waiting, but has been
953                     # reserved by some other patron.
954                     $needsconfirmation{RESERVE_WAITING} = 1;
955                     $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
956                     $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
957                     $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
958                     $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
959                     $needsconfirmation{'resbranchname'} = $branchname;
960                     $needsconfirmation{'reswaitingdate'} = format_date($res->{'waitingdate'});
961                 }
962                 elsif ( $restype eq "Reserved" ) {
963                     # The item is on reserve for someone else.
964                     $needsconfirmation{RESERVED} = 1;
965                     $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
966                     $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
967                     $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
968                     $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
969                     $needsconfirmation{'resbranchname'} = $branchname;
970                     $needsconfirmation{'resreservedate'} = format_date($res->{'reservedate'});
971                 }
972             }
973         }
974     }
975
976     ## CHECK AGE RESTRICTION
977     # get $marker from preferences. Could be something like "FSK|PEGI|Alter|Age:"
978     my $markers         = C4::Context->preference('AgeRestrictionMarker');
979     my $bibvalues       = $biblioitem->{'agerestriction'};
980     my $restriction_age = GetAgeRestriction( $bibvalues );
981
982     if ( $restriction_age > 0 ) {
983         if ( $borrower->{'dateofbirth'} ) {
984             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
985             $alloweddate[0] += $restriction_age;
986
987             #Prevent runime eror on leap year (invalid date)
988             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
989                 $alloweddate[2] = 28;
990             }
991
992             if ( Date_to_Days(Today) < Date_to_Days(@alloweddate) - 1 ) {
993                 if ( C4::Context->preference('AgeRestrictionOverride') ) {
994                     $needsconfirmation{AGE_RESTRICTION} = "$bibvalues";
995                 }
996                 else {
997                     $issuingimpossible{AGE_RESTRICTION} = "$bibvalues";
998                 }
999             }
1000         }
1001     }
1002
1003     ## check for high holds decreasing loan period
1004     my $decrease_loan = C4::Context->preference('decreaseLoanHighHolds');
1005     if ( $decrease_loan && $decrease_loan == 1 ) {
1006         my ( $reserved, $num, $duration, $returndate ) =
1007           checkHighHolds( $item, $borrower );
1008
1009         if ( $num >= C4::Context->preference('decreaseLoanHighHoldsValue') ) {
1010             $needsconfirmation{HIGHHOLDS} = {
1011                 num_holds  => $num,
1012                 duration   => $duration,
1013                 returndate => output_pref($returndate),
1014             };
1015         }
1016     }
1017
1018     if (not C4::Context->preference('AllowMultipleIssuesOnABiblio')) {
1019         # Check if borrower has already issued an item from the same biblio
1020         # Only if it's not a subscription
1021         my $biblionumber = $item->{biblionumber};
1022         require C4::Serials;
1023         my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1024         unless ($is_a_subscription) {
1025             my $issues = GetIssues( {
1026                 borrowernumber => $borrower->{borrowernumber},
1027                 biblionumber   => $biblionumber,
1028             } );
1029             my @issues = $issues ? @$issues : ();
1030             # If there is at least one issue on another item than the item we want to checkout
1031             if (scalar @issues > 0 and $issues[0]->{itemnumber} != $item->{itemnumber}) {
1032                 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1033             }
1034         }
1035     }
1036
1037     return ( \%issuingimpossible, \%needsconfirmation, \%alerts );
1038 }
1039
1040 =head2 CanBookBeReturned
1041
1042   ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1043
1044 Check whether the item can be returned to the provided branch
1045
1046 =over 4
1047
1048 =item C<$item> is a hash of item information as returned from GetItem
1049
1050 =item C<$branch> is the branchcode where the return is taking place
1051
1052 =back
1053
1054 Returns:
1055
1056 =over 4
1057
1058 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1059
1060 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1061
1062 =back
1063
1064 =cut
1065
1066 sub CanBookBeReturned {
1067   my ($item, $branch) = @_;
1068   my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1069
1070   # assume return is allowed to start
1071   my $allowed = 1;
1072   my $message;
1073
1074   # identify all cases where return is forbidden
1075   if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1076      $allowed = 0;
1077      $message = $item->{'homebranch'};
1078   } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1079      $allowed = 0;
1080      $message = $item->{'holdingbranch'};
1081   } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1082      $allowed = 0;
1083      $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1084   }
1085
1086   return ($allowed, $message);
1087 }
1088
1089 =head2 CheckHighHolds
1090
1091     used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1092     decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1093     has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1094
1095 =cut
1096
1097 sub checkHighHolds {
1098     my ( $item, $borrower ) = @_;
1099     my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1100     my $branch = _GetCircControlBranch( $item, $borrower );
1101     my $dbh    = C4::Context->dbh;
1102     my $sth    = $dbh->prepare(
1103 'select count(borrowernumber) as num_holds from reserves where biblionumber=?'
1104     );
1105     $sth->execute( $item->{'biblionumber'} );
1106     my ($holds) = $sth->fetchrow_array;
1107     if ($holds) {
1108         my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1109
1110         my $calendar = Koha::Calendar->new( branchcode => $branch );
1111
1112         my $itype =
1113           ( C4::Context->preference('item-level_itypes') )
1114           ? $biblio->{'itype'}
1115           : $biblio->{'itemtype'};
1116         my $orig_due =
1117           C4::Circulation::CalcDateDue( $issuedate, $itype, $branch,
1118             $borrower );
1119
1120         my $reduced_datedue =
1121           $calendar->addDate( $issuedate,
1122             C4::Context->preference('decreaseLoanHighHoldsDuration') );
1123
1124         if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1125             return ( 1, $holds,
1126                 C4::Context->preference('decreaseLoanHighHoldsDuration'),
1127                 $reduced_datedue );
1128         }
1129     }
1130     return ( 0, 0, 0, undef );
1131 }
1132
1133 =head2 AddIssue
1134
1135   &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1136
1137 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1138
1139 =over 4
1140
1141 =item C<$borrower> is a hash with borrower informations (from GetMember or GetMemberDetails).
1142
1143 =item C<$barcode> is the barcode of the item being issued.
1144
1145 =item C<$datedue> is a C4::Dates object for the max date of return, i.e. the date due (optional).
1146 Calculated if empty.
1147
1148 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1149
1150 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1151 Defaults to today.  Unlike C<$datedue>, NOT a C4::Dates object, unfortunately.
1152
1153 AddIssue does the following things :
1154
1155   - step 01: check that there is a borrowernumber & a barcode provided
1156   - check for RENEWAL (book issued & being issued to the same patron)
1157       - renewal YES = Calculate Charge & renew
1158       - renewal NO  =
1159           * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1160           * RESERVE PLACED ?
1161               - fill reserve if reserve to this patron
1162               - cancel reserve or not, otherwise
1163           * TRANSFERT PENDING ?
1164               - complete the transfert
1165           * ISSUE THE BOOK
1166
1167 =back
1168
1169 =cut
1170
1171 sub AddIssue {
1172     my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode) = @_;
1173     my $dbh = C4::Context->dbh;
1174         my $barcodecheck=CheckValidBarcode($barcode);
1175     if ($datedue && ref $datedue ne 'DateTime') {
1176         $datedue = dt_from_string($datedue);
1177     }
1178     # $issuedate defaults to today.
1179     if ( ! defined $issuedate ) {
1180         $issuedate = DateTime->now(time_zone => C4::Context->tz());
1181     }
1182     else {
1183         if ( ref $issuedate ne 'DateTime') {
1184             $issuedate = dt_from_string($issuedate);
1185
1186         }
1187     }
1188         if ($borrower and $barcode and $barcodecheck ne '0'){#??? wtf
1189                 # find which item we issue
1190                 my $item = GetItem('', $barcode) or return;     # if we don't get an Item, abort.
1191                 my $branch = _GetCircControlBranch($item,$borrower);
1192                 
1193                 # get actual issuing if there is one
1194                 my $actualissue = GetItemIssue( $item->{itemnumber});
1195                 
1196                 # get biblioinformation for this item
1197                 my $biblio = GetBiblioFromItemNumber($item->{itemnumber});
1198                 
1199                 #
1200                 # check if we just renew the issue.
1201                 #
1202                 if ($actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}) {
1203                     $datedue = AddRenewal(
1204                         $borrower->{'borrowernumber'},
1205                         $item->{'itemnumber'},
1206                         $branch,
1207                         $datedue,
1208                         $issuedate, # here interpreted as the renewal date
1209                         );
1210                 }
1211                 else {
1212         # it's NOT a renewal
1213                         if ( $actualissue->{borrowernumber}) {
1214                                 # This book is currently on loan, but not to the person
1215                                 # who wants to borrow it now. mark it returned before issuing to the new borrower
1216                                 AddReturn(
1217                                         $item->{'barcode'},
1218                                         C4::Context->userenv->{'branch'}
1219                                 );
1220                         }
1221
1222             MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1223                         # Starting process for transfer job (checking transfert and validate it if we have one)
1224             my ($datesent) = GetTransfers($item->{'itemnumber'});
1225             if ($datesent) {
1226         #       updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1227                 my $sth =
1228                     $dbh->prepare(
1229                     "UPDATE branchtransfers 
1230                         SET datearrived = now(),
1231                         tobranch = ?,
1232                         comments = 'Forced branchtransfer'
1233                     WHERE itemnumber= ? AND datearrived IS NULL"
1234                     );
1235                 $sth->execute(C4::Context->userenv->{'branch'},$item->{'itemnumber'});
1236             }
1237
1238         # Record in the database the fact that the book was issued.
1239         my $sth =
1240           $dbh->prepare(
1241                 "INSERT INTO issues
1242                     (borrowernumber, itemnumber,issuedate, date_due, branchcode)
1243                 VALUES (?,?,?,?,?)"
1244           );
1245         unless ($datedue) {
1246             my $itype = ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'};
1247             $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1248
1249         }
1250         $datedue->truncate( to => 'minute');
1251         $sth->execute(
1252             $borrower->{'borrowernumber'},      # borrowernumber
1253             $item->{'itemnumber'},              # itemnumber
1254             $issuedate->strftime('%Y-%m-%d %H:%M:00'), # issuedate
1255             $datedue->strftime('%Y-%m-%d %H:%M:00'),   # date_due
1256             C4::Context->userenv->{'branch'}    # branchcode
1257         );
1258         if ( C4::Context->preference('ReturnToShelvingCart') ) { ## ReturnToShelvingCart is on, anything issued should be taken off the cart.
1259           CartToShelf( $item->{'itemnumber'} );
1260         }
1261         $item->{'issues'}++;
1262         if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1263             UpdateTotalIssues($item->{'biblionumber'}, 1);
1264         }
1265
1266         ## If item was lost, it has now been found, reverse any list item charges if neccessary.
1267         if ( $item->{'itemlost'} ) {
1268             if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) {
1269                 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, $item->{'barcode'} );
1270             }
1271         }
1272
1273         ModItem({ issues           => $item->{'issues'},
1274                   holdingbranch    => C4::Context->userenv->{'branch'},
1275                   itemlost         => 0,
1276                   datelastborrowed => DateTime->now(time_zone => C4::Context->tz())->ymd(),
1277                   onloan           => $datedue->ymd(),
1278                 }, $item->{'biblionumber'}, $item->{'itemnumber'});
1279         ModDateLastSeen( $item->{'itemnumber'} );
1280
1281         # If it costs to borrow this book, charge it to the patron's account.
1282         my ( $charge, $itemtype ) = GetIssuingCharges(
1283             $item->{'itemnumber'},
1284             $borrower->{'borrowernumber'}
1285         );
1286         if ( $charge > 0 ) {
1287             AddIssuingCharge(
1288                 $item->{'itemnumber'},
1289                 $borrower->{'borrowernumber'}, $charge
1290             );
1291             $item->{'charge'} = $charge;
1292         }
1293
1294         # Record the fact that this book was issued.
1295         &UpdateStats(
1296             C4::Context->userenv->{'branch'},
1297             'issue', $charge,
1298             ($sipmode ? "SIP-$sipmode" : ''), $item->{'itemnumber'},
1299             $item->{'itype'}, $borrower->{'borrowernumber'}, undef, $item->{'ccode'}
1300         );
1301
1302         # Send a checkout slip.
1303         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1304         my %conditions = (
1305             branchcode   => $branch,
1306             categorycode => $borrower->{categorycode},
1307             item_type    => $item->{itype},
1308             notification => 'CHECKOUT',
1309         );
1310         if ($circulation_alert->is_enabled_for(\%conditions)) {
1311             SendCirculationAlert({
1312                 type     => 'CHECKOUT',
1313                 item     => $item,
1314                 borrower => $borrower,
1315                 branch   => $branch,
1316             });
1317         }
1318     }
1319
1320     logaction("CIRCULATION", "ISSUE", $borrower->{'borrowernumber'}, $biblio->{'itemnumber'})
1321         if C4::Context->preference("IssueLog");
1322   }
1323   return ($datedue);    # not necessarily the same as when it came in!
1324 }
1325
1326 =head2 GetLoanLength
1327
1328   my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1329
1330 Get loan length for an itemtype, a borrower type and a branch
1331
1332 =cut
1333
1334 sub GetLoanLength {
1335     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1336     my $dbh = C4::Context->dbh;
1337     my $sth = $dbh->prepare(qq{
1338         SELECT issuelength, lengthunit, renewalperiod
1339         FROM issuingrules
1340         WHERE   categorycode=?
1341             AND itemtype=?
1342             AND branchcode=?
1343             AND issuelength IS NOT NULL
1344     });
1345
1346     # try to find issuelength & return the 1st available.
1347     # check with borrowertype, itemtype and branchcode, then without one of those parameters
1348     $sth->execute( $borrowertype, $itemtype, $branchcode );
1349     my $loanlength = $sth->fetchrow_hashref;
1350
1351     return $loanlength
1352       if defined($loanlength) && $loanlength->{issuelength};
1353
1354     $sth->execute( $borrowertype, '*', $branchcode );
1355     $loanlength = $sth->fetchrow_hashref;
1356     return $loanlength
1357       if defined($loanlength) && $loanlength->{issuelength};
1358
1359     $sth->execute( '*', $itemtype, $branchcode );
1360     $loanlength = $sth->fetchrow_hashref;
1361     return $loanlength
1362       if defined($loanlength) && $loanlength->{issuelength};
1363
1364     $sth->execute( '*', '*', $branchcode );
1365     $loanlength = $sth->fetchrow_hashref;
1366     return $loanlength
1367       if defined($loanlength) && $loanlength->{issuelength};
1368
1369     $sth->execute( $borrowertype, $itemtype, '*' );
1370     $loanlength = $sth->fetchrow_hashref;
1371     return $loanlength
1372       if defined($loanlength) && $loanlength->{issuelength};
1373
1374     $sth->execute( $borrowertype, '*', '*' );
1375     $loanlength = $sth->fetchrow_hashref;
1376     return $loanlength
1377       if defined($loanlength) && $loanlength->{issuelength};
1378
1379     $sth->execute( '*', $itemtype, '*' );
1380     $loanlength = $sth->fetchrow_hashref;
1381     return $loanlength
1382       if defined($loanlength) && $loanlength->{issuelength};
1383
1384     $sth->execute( '*', '*', '*' );
1385     $loanlength = $sth->fetchrow_hashref;
1386     return $loanlength
1387       if defined($loanlength) && $loanlength->{issuelength};
1388
1389     # if no rule is set => 21 days (hardcoded)
1390     return {
1391         issuelength => 21,
1392         renewalperiod => 21,
1393         lengthunit => 'days',
1394     };
1395
1396 }
1397
1398
1399 =head2 GetHardDueDate
1400
1401   my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1402
1403 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1404
1405 =cut
1406
1407 sub GetHardDueDate {
1408     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1409
1410     my $rule = GetIssuingRule( $borrowertype, $itemtype, $branchcode );
1411
1412     if ( defined( $rule ) ) {
1413         if ( $rule->{hardduedate} ) {
1414             return (dt_from_string($rule->{hardduedate}, 'iso'),$rule->{hardduedatecompare});
1415         } else {
1416             return (undef, undef);
1417         }
1418     }
1419 }
1420
1421 =head2 GetIssuingRule
1422
1423   my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
1424
1425 FIXME - This is a copy-paste of GetLoanLength
1426 as a stop-gap.  Do not wish to change API for GetLoanLength 
1427 this close to release.
1428
1429 Get the issuing rule for an itemtype, a borrower type and a branch
1430 Returns a hashref from the issuingrules table.
1431
1432 =cut
1433
1434 sub GetIssuingRule {
1435     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1436     my $dbh = C4::Context->dbh;
1437     my $sth =  $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null"  );
1438     my $irule;
1439
1440         $sth->execute( $borrowertype, $itemtype, $branchcode );
1441     $irule = $sth->fetchrow_hashref;
1442     return $irule if defined($irule) ;
1443
1444     $sth->execute( $borrowertype, "*", $branchcode );
1445     $irule = $sth->fetchrow_hashref;
1446     return $irule if defined($irule) ;
1447
1448     $sth->execute( "*", $itemtype, $branchcode );
1449     $irule = $sth->fetchrow_hashref;
1450     return $irule if defined($irule) ;
1451
1452     $sth->execute( "*", "*", $branchcode );
1453     $irule = $sth->fetchrow_hashref;
1454     return $irule if defined($irule) ;
1455
1456     $sth->execute( $borrowertype, $itemtype, "*" );
1457     $irule = $sth->fetchrow_hashref;
1458     return $irule if defined($irule) ;
1459
1460     $sth->execute( $borrowertype, "*", "*" );
1461     $irule = $sth->fetchrow_hashref;
1462     return $irule if defined($irule) ;
1463
1464     $sth->execute( "*", $itemtype, "*" );
1465     $irule = $sth->fetchrow_hashref;
1466     return $irule if defined($irule) ;
1467
1468     $sth->execute( "*", "*", "*" );
1469     $irule = $sth->fetchrow_hashref;
1470     return $irule if defined($irule) ;
1471
1472     # if no rule matches,
1473     return;
1474 }
1475
1476 =head2 GetBranchBorrowerCircRule
1477
1478   my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1479
1480 Retrieves circulation rule attributes that apply to the given
1481 branch and patron category, regardless of item type.  
1482 The return value is a hashref containing the following key:
1483
1484 maxissueqty - maximum number of loans that a
1485 patron of the given category can have at the given
1486 branch.  If the value is undef, no limit.
1487
1488 This will first check for a specific branch and
1489 category match from branch_borrower_circ_rules. 
1490
1491 If no rule is found, it will then check default_branch_circ_rules
1492 (same branch, default category).  If no rule is found,
1493 it will then check default_borrower_circ_rules (default 
1494 branch, same category), then failing that, default_circ_rules
1495 (default branch, default category).
1496
1497 If no rule has been found in the database, it will default to
1498 the buillt in rule:
1499
1500 maxissueqty - undef
1501
1502 C<$branchcode> and C<$categorycode> should contain the
1503 literal branch code and patron category code, respectively - no
1504 wildcards.
1505
1506 =cut
1507
1508 sub GetBranchBorrowerCircRule {
1509     my $branchcode = shift;
1510     my $categorycode = shift;
1511
1512     my $branch_cat_query = "SELECT maxissueqty
1513                             FROM branch_borrower_circ_rules
1514                             WHERE branchcode = ?
1515                             AND   categorycode = ?";
1516     my $dbh = C4::Context->dbh();
1517     my $sth = $dbh->prepare($branch_cat_query);
1518     $sth->execute($branchcode, $categorycode);
1519     my $result;
1520     if ($result = $sth->fetchrow_hashref()) {
1521         return $result;
1522     }
1523
1524     # try same branch, default borrower category
1525     my $branch_query = "SELECT maxissueqty
1526                         FROM default_branch_circ_rules
1527                         WHERE branchcode = ?";
1528     $sth = $dbh->prepare($branch_query);
1529     $sth->execute($branchcode);
1530     if ($result = $sth->fetchrow_hashref()) {
1531         return $result;
1532     }
1533
1534     # try default branch, same borrower category
1535     my $category_query = "SELECT maxissueqty
1536                           FROM default_borrower_circ_rules
1537                           WHERE categorycode = ?";
1538     $sth = $dbh->prepare($category_query);
1539     $sth->execute($categorycode);
1540     if ($result = $sth->fetchrow_hashref()) {
1541         return $result;
1542     }
1543   
1544     # try default branch, default borrower category
1545     my $default_query = "SELECT maxissueqty
1546                           FROM default_circ_rules";
1547     $sth = $dbh->prepare($default_query);
1548     $sth->execute();
1549     if ($result = $sth->fetchrow_hashref()) {
1550         return $result;
1551     }
1552     
1553     # built-in default circulation rule
1554     return {
1555         maxissueqty => undef,
1556     };
1557 }
1558
1559 =head2 GetBranchItemRule
1560
1561   my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1562
1563 Retrieves circulation rule attributes that apply to the given
1564 branch and item type, regardless of patron category.
1565
1566 The return value is a hashref containing the following keys:
1567
1568 holdallowed => Hold policy for this branch and itemtype. Possible values:
1569   0: No holds allowed.
1570   1: Holds allowed only by patrons that have the same homebranch as the item.
1571   2: Holds allowed from any patron.
1572
1573 returnbranch => branch to which to return item.  Possible values:
1574   noreturn: do not return, let item remain where checked in (floating collections)
1575   homebranch: return to item's home branch
1576
1577 This searches branchitemrules in the following order:
1578
1579   * Same branchcode and itemtype
1580   * Same branchcode, itemtype '*'
1581   * branchcode '*', same itemtype
1582   * branchcode and itemtype '*'
1583
1584 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1585
1586 =cut
1587
1588 sub GetBranchItemRule {
1589     my ( $branchcode, $itemtype ) = @_;
1590     my $dbh = C4::Context->dbh();
1591     my $result = {};
1592
1593     my @attempts = (
1594         ['SELECT holdallowed, returnbranch
1595             FROM branch_item_rules
1596             WHERE branchcode = ?
1597               AND itemtype = ?', $branchcode, $itemtype],
1598         ['SELECT holdallowed, returnbranch
1599             FROM default_branch_circ_rules
1600             WHERE branchcode = ?', $branchcode],
1601         ['SELECT holdallowed, returnbranch
1602             FROM default_branch_item_rules
1603             WHERE itemtype = ?', $itemtype],
1604         ['SELECT holdallowed, returnbranch
1605             FROM default_circ_rules'],
1606     );
1607
1608     foreach my $attempt (@attempts) {
1609         my ($query, @bind_params) = @{$attempt};
1610         my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1611           or next;
1612
1613         # Since branch/category and branch/itemtype use the same per-branch
1614         # defaults tables, we have to check that the key we want is set, not
1615         # just that a row was returned
1616         $result->{'holdallowed'}  = $search_result->{'holdallowed'}  unless ( defined $result->{'holdallowed'} );
1617         $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1618     }
1619     
1620     # built-in default circulation rule
1621     $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1622     $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1623
1624     return $result;
1625 }
1626
1627 =head2 AddReturn
1628
1629   ($doreturn, $messages, $iteminformation, $borrower) =
1630       &AddReturn($barcode, $branch, $exemptfine, $dropbox);
1631
1632 Returns a book.
1633
1634 =over 4
1635
1636 =item C<$barcode> is the bar code of the book being returned.
1637
1638 =item C<$branch> is the code of the branch where the book is being returned.
1639
1640 =item C<$exemptfine> indicates that overdue charges for the item will be
1641 removed.
1642
1643 =item C<$dropbox> indicates that the check-in date is assumed to be
1644 yesterday, or the last non-holiday as defined in C4::Calendar .  If
1645 overdue charges are applied and C<$dropbox> is true, the last charge
1646 will be removed.  This assumes that the fines accrual script has run
1647 for _today_.
1648
1649 =back
1650
1651 C<&AddReturn> returns a list of four items:
1652
1653 C<$doreturn> is true iff the return succeeded.
1654
1655 C<$messages> is a reference-to-hash giving feedback on the operation.
1656 The keys of the hash are:
1657
1658 =over 4
1659
1660 =item C<BadBarcode>
1661
1662 No item with this barcode exists. The value is C<$barcode>.
1663
1664 =item C<NotIssued>
1665
1666 The book is not currently on loan. The value is C<$barcode>.
1667
1668 =item C<IsPermanent>
1669
1670 The book's home branch is a permanent collection. If you have borrowed
1671 this book, you are not allowed to return it. The value is the code for
1672 the book's home branch.
1673
1674 =item C<withdrawn>
1675
1676 This book has been withdrawn/cancelled. The value should be ignored.
1677
1678 =item C<Wrongbranch>
1679
1680 This book has was returned to the wrong branch.  The value is a hashref
1681 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1682 contain the branchcode of the incorrect and correct return library, respectively.
1683
1684 =item C<ResFound>
1685
1686 The item was reserved. The value is a reference-to-hash whose keys are
1687 fields from the reserves table of the Koha database, and
1688 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1689 either C<Waiting>, C<Reserved>, or 0.
1690
1691 =back
1692
1693 C<$iteminformation> is a reference-to-hash, giving information about the
1694 returned item from the issues table.
1695
1696 C<$borrower> is a reference-to-hash, giving information about the
1697 patron who last borrowed the book.
1698
1699 =cut
1700
1701 sub AddReturn {
1702     my ( $barcode, $branch, $exemptfine, $dropbox ) = @_;
1703
1704     if ($branch and not GetBranchDetail($branch)) {
1705         warn "AddReturn error: branch '$branch' not found.  Reverting to " . C4::Context->userenv->{'branch'};
1706         undef $branch;
1707     }
1708     $branch = C4::Context->userenv->{'branch'} unless $branch;  # we trust userenv to be a safe fallback/default
1709     my $messages;
1710     my $borrower;
1711     my $biblio;
1712     my $doreturn       = 1;
1713     my $validTransfert = 0;
1714     my $stat_type = 'return';    
1715
1716     # get information on item
1717     my $itemnumber = GetItemnumberFromBarcode( $barcode );
1718     unless ($itemnumber) {
1719         return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower.  bail out.
1720     }
1721     my $issue  = GetItemIssue($itemnumber);
1722 #   warn Dumper($iteminformation);
1723     if ($issue and $issue->{borrowernumber}) {
1724         $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1725             or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n"
1726                 . Dumper($issue) . "\n";
1727     } else {
1728         $messages->{'NotIssued'} = $barcode;
1729         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
1730         $doreturn = 0;
1731         # No issue, no borrowernumber.  ONLY if $doreturn, *might* you have a $borrower later.
1732         # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1733         if (C4::Context->preference("RecordLocalUseOnReturn")) {
1734            $messages->{'LocalUse'} = 1;
1735            $stat_type = 'localuse';
1736         }
1737     }
1738
1739     my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1740         # full item data, but no borrowernumber or checkout info (no issue)
1741         # we know GetItem should work because GetItemnumberFromBarcode worked
1742     my $hbr      = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1743         # get the proper branch to which to return the item
1744     $hbr = $item->{$hbr} || $branch ;
1745         # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1746
1747     my $borrowernumber = $borrower->{'borrowernumber'} || undef;    # we don't know if we had a borrower or not
1748
1749     # check if the book is in a permanent collection....
1750     # FIXME -- This 'PE' attribute is largely undocumented.  afaict, there's no user interface that reflects this functionality.
1751     if ( $hbr ) {
1752         my $branches = GetBranches();    # a potentially expensive call for a non-feature.
1753         $branches->{$hbr}->{PE} and $messages->{'IsPermanent'} = $hbr;
1754     }
1755
1756     # check if the return is allowed at this branch
1757     my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1758     unless ($returnallowed){
1759         $messages->{'Wrongbranch'} = {
1760             Wrongbranch => $branch,
1761             Rightbranch => $message
1762         };
1763         $doreturn = 0;
1764         return ( $doreturn, $messages, $issue, $borrower );
1765     }
1766
1767     if ( $item->{'withdrawn'} ) { # book has been cancelled
1768         $messages->{'withdrawn'} = 1;
1769         $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1770     }
1771
1772     # case of a return of document (deal with issues and holdingbranch)
1773     my $today = DateTime->now( time_zone => C4::Context->tz() );
1774     if ($doreturn) {
1775     my $datedue = $issue->{date_due};
1776         $borrower or warn "AddReturn without current borrower";
1777                 my $circControlBranch;
1778         if ($dropbox) {
1779             # define circControlBranch only if dropbox mode is set
1780             # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1781             # FIXME: check issuedate > returndate, factoring in holidays
1782             #$circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );;
1783             $circControlBranch = _GetCircControlBranch($item,$borrower);
1784         $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $today ) == -1 ? 1 : 0;
1785         }
1786
1787         if ($borrowernumber) {
1788             if( C4::Context->preference('CalculateFinesOnReturn') && $issue->{'overdue'}){
1789             # we only need to calculate and change the fines if we want to do that on return
1790             # Should be on for hourly loans
1791                 my $control = C4::Context->preference('CircControl');
1792                 my $control_branchcode =
1793                     ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
1794                   : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
1795                   :                                     $issue->{branchcode};
1796
1797                 my ( $amount, $type, $unitcounttotal ) =
1798                   C4::Overdues::CalcFine( $item, $borrower->{categorycode},
1799                     $control_branchcode, $datedue, $today );
1800
1801                 $type ||= q{};
1802
1803                 if ( $amount > 0
1804                     && C4::Context->preference('finesMode') eq 'production' )
1805                 {
1806                     C4::Overdues::UpdateFine( $issue->{itemnumber},
1807                         $issue->{borrowernumber},
1808                         $amount, $type, output_pref($datedue) );
1809                 }
1810             }
1811
1812             MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
1813                 $circControlBranch, '', $borrower->{'privacy'} );
1814
1815             # FIXME is the "= 1" right?  This could be the borrower hash.
1816             $messages->{'WasReturned'} = 1;
1817
1818         }
1819
1820         ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1821     }
1822
1823     # the holdingbranch is updated if the document is returned to another location.
1824     # this is always done regardless of whether the item was on loan or not
1825     if ($item->{'holdingbranch'} ne $branch) {
1826         UpdateHoldingbranch($branch, $item->{'itemnumber'});
1827         $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1828     }
1829     ModDateLastSeen( $item->{'itemnumber'} );
1830
1831     # check if we have a transfer for this document
1832     my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1833
1834     # if we have a transfer to do, we update the line of transfers with the datearrived
1835     if ($datesent) {
1836         if ( $tobranch eq $branch ) {
1837             my $sth = C4::Context->dbh->prepare(
1838                 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1839             );
1840             $sth->execute( $item->{'itemnumber'} );
1841             # if we have a reservation with valid transfer, we can set it's status to 'W'
1842             ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1843             C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1844         } else {
1845             $messages->{'WrongTransfer'}     = $tobranch;
1846             $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1847         }
1848         $validTransfert = 1;
1849     } else {
1850         ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1851     }
1852
1853     # fix up the accounts.....
1854     if ( $item->{'itemlost'} ) {
1855         $messages->{'WasLost'} = 1;
1856
1857         if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) {
1858             _FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode);    # can tolerate undef $borrowernumber
1859             $messages->{'LostItemFeeRefunded'} = 1;
1860         }
1861     }
1862
1863     # fix up the overdues in accounts...
1864     if ($borrowernumber) {
1865         my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
1866         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!";  # zero is OK, check defined
1867         
1868         if ( $issue->{overdue} && $issue->{date_due} ) {
1869 # fix fine days
1870             my $debardate =
1871               _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
1872             $messages->{Debarred} = $debardate if ($debardate);
1873         }
1874     }
1875
1876     # find reserves.....
1877     # if we don't have a reserve with the status W, we launch the Checkreserves routine
1878     my ($resfound, $resrec);
1879     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1880     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'}, undef, $lookahead ) unless ( $item->{'withdrawn'} );
1881     if ($resfound) {
1882           $resrec->{'ResFound'} = $resfound;
1883         $messages->{'ResFound'} = $resrec;
1884     }
1885
1886     # update stats?
1887     # Record the fact that this book was returned.
1888     UpdateStats(
1889         $branch, $stat_type, '0', '',
1890         $item->{'itemnumber'},
1891         $biblio->{'itemtype'},
1892         $borrowernumber, undef, $item->{'ccode'}
1893     );
1894
1895     # Send a check-in slip. # NOTE: borrower may be undef.  probably shouldn't try to send messages then.
1896     my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1897     my %conditions = (
1898         branchcode   => $branch,
1899         categorycode => $borrower->{categorycode},
1900         item_type    => $item->{itype},
1901         notification => 'CHECKIN',
1902     );
1903     if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
1904         SendCirculationAlert({
1905             type     => 'CHECKIN',
1906             item     => $item,
1907             borrower => $borrower,
1908             branch   => $branch,
1909         });
1910     }
1911     
1912     logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
1913         if C4::Context->preference("ReturnLog");
1914     
1915     # Remove any OVERDUES related debarment if the borrower has no overdues
1916     if ( $borrowernumber
1917       && $borrower->{'debarred'}
1918       && C4::Context->preference('AutoRemoveOverduesRestrictions')
1919       && !HasOverdues( $borrowernumber )
1920       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
1921     ) {
1922         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
1923     }
1924
1925     # FIXME: make this comment intelligible.
1926     #adding message if holdingbranch is non equal a userenv branch to return the document to homebranch
1927     #we check, if we don't have reserv or transfert for this document, if not, return it to homebranch .
1928
1929     if (($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $hbr) and not $messages->{'WrongTransfer'}){
1930         if ( C4::Context->preference("AutomaticItemReturn"    ) or
1931             (C4::Context->preference("UseBranchTransferLimits") and
1932              ! IsBranchTransferAllowed($branch, $hbr, $item->{C4::Context->preference("BranchTransferLimitsType")} )
1933            )) {
1934             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $hbr;
1935             $debug and warn "item: " . Dumper($item);
1936             ModItemTransfer($item->{'itemnumber'}, $branch, $hbr);
1937             $messages->{'WasTransfered'} = 1;
1938         } else {
1939             $messages->{'NeedsTransfer'} = 1;   # TODO: instead of 1, specify branchcode that the transfer SHOULD go to, $item->{homebranch}
1940         }
1941     }
1942     return ( $doreturn, $messages, $issue, $borrower );
1943 }
1944
1945 =head2 MarkIssueReturned
1946
1947   MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
1948
1949 Unconditionally marks an issue as being returned by
1950 moving the C<issues> row to C<old_issues> and
1951 setting C<returndate> to the current date, or
1952 the last non-holiday date of the branccode specified in
1953 C<dropbox_branch> .  Assumes you've already checked that 
1954 it's safe to do this, i.e. last non-holiday > issuedate.
1955
1956 if C<$returndate> is specified (in iso format), it is used as the date
1957 of the return. It is ignored when a dropbox_branch is passed in.
1958
1959 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
1960 the old_issue is immediately anonymised
1961
1962 Ideally, this function would be internal to C<C4::Circulation>,
1963 not exported, but it is currently needed by one 
1964 routine in C<C4::Accounts>.
1965
1966 =cut
1967
1968 sub MarkIssueReturned {
1969     my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
1970
1971     my $dbh   = C4::Context->dbh;
1972     my $query = 'UPDATE issues SET returndate=';
1973     my @bind;
1974     if ($dropbox_branch) {
1975         my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
1976         my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
1977         $query .= ' ? ';
1978         push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
1979     } elsif ($returndate) {
1980         $query .= ' ? ';
1981         push @bind, $returndate;
1982     } else {
1983         $query .= ' now() ';
1984     }
1985     $query .= ' WHERE  borrowernumber = ?  AND itemnumber = ?';
1986     push @bind, $borrowernumber, $itemnumber;
1987     # FIXME transaction
1988     my $sth_upd  = $dbh->prepare($query);
1989     $sth_upd->execute(@bind);
1990     my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
1991                                   WHERE borrowernumber = ?
1992                                   AND itemnumber = ?');
1993     $sth_copy->execute($borrowernumber, $itemnumber);
1994     # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
1995     if ( $privacy == 2) {
1996         # The default of 0 does not work due to foreign key constraints
1997         # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
1998         # FIXME the above is unacceptable - bug 9942 relates
1999         my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
2000         my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
2001                                   WHERE borrowernumber = ?
2002                                   AND itemnumber = ?");
2003        $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
2004     }
2005     my $sth_del  = $dbh->prepare("DELETE FROM issues
2006                                   WHERE borrowernumber = ?
2007                                   AND itemnumber = ?");
2008     $sth_del->execute($borrowernumber, $itemnumber);
2009 }
2010
2011 =head2 _debar_user_on_return
2012
2013     _debar_user_on_return($borrower, $item, $datedue, today);
2014
2015 C<$borrower> borrower hashref
2016
2017 C<$item> item hashref
2018
2019 C<$datedue> date due DateTime object
2020
2021 C<$today> DateTime object representing the return time
2022
2023 Internal function, called only by AddReturn that calculates and updates
2024  the user fine days, and debars him if necessary.
2025
2026 Should only be called for overdue returns
2027
2028 =cut
2029
2030 sub _debar_user_on_return {
2031     my ( $borrower, $item, $dt_due, $dt_today ) = @_;
2032
2033     my $branchcode = _GetCircControlBranch( $item, $borrower );
2034     my $calendar = Koha::Calendar->new( branchcode => $branchcode );
2035
2036     # $deltadays is a DateTime::Duration object
2037     my $deltadays = $calendar->days_between( $dt_due, $dt_today );
2038
2039     my $circcontrol = C4::Context->preference('CircControl');
2040     my $issuingrule =
2041       GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2042     my $finedays = $issuingrule->{finedays};
2043     my $unit     = $issuingrule->{lengthunit};
2044
2045     if ($finedays) {
2046
2047         # finedays is in days, so hourly loans must multiply by 24
2048         # thus 1 hour late equals 1 day suspension * finedays rate
2049         $finedays = $finedays * 24 if ( $unit eq 'hours' );
2050
2051         # grace period is measured in the same units as the loan
2052         my $grace =
2053           DateTime::Duration->new( $unit => $issuingrule->{firstremind} );
2054         if ( $deltadays->subtract($grace)->is_positive() ) {
2055
2056             my $new_debar_dt =
2057               $dt_today->clone()->add_duration( $deltadays * $finedays );
2058
2059             Koha::Borrower::Debarments::AddUniqueDebarment({
2060                 borrowernumber => $borrower->{borrowernumber},
2061                 expiration     => $new_debar_dt->ymd(),
2062                 type           => 'SUSPENSION',
2063             });
2064
2065             return $new_debar_dt->ymd();
2066         }
2067     }
2068     return;
2069 }
2070
2071 =head2 _FixOverduesOnReturn
2072
2073    &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2074
2075 C<$brn> borrowernumber
2076
2077 C<$itm> itemnumber
2078
2079 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2080 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2081
2082 Internal function, called only by AddReturn
2083
2084 =cut
2085
2086 sub _FixOverduesOnReturn {
2087     my ($borrowernumber, $item);
2088     unless ($borrowernumber = shift) {
2089         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2090         return;
2091     }
2092     unless ($item = shift) {
2093         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2094         return;
2095     }
2096     my ($exemptfine, $dropbox) = @_;
2097     my $dbh = C4::Context->dbh;
2098
2099     # check for overdue fine
2100     my $sth = $dbh->prepare(
2101 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
2102     );
2103     $sth->execute( $borrowernumber, $item );
2104
2105     # alter fine to show that the book has been returned
2106     my $data = $sth->fetchrow_hashref;
2107     return 0 unless $data;    # no warning, there's just nothing to fix
2108
2109     my $uquery;
2110     my @bind = ($data->{'accountlines_id'});
2111     if ($exemptfine) {
2112         $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2113         if (C4::Context->preference("FinesLog")) {
2114             &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2115         }
2116     } elsif ($dropbox && $data->{lastincrement}) {
2117         my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2118         my $amt = $data->{amount} - $data->{lastincrement} ;
2119         if (C4::Context->preference("FinesLog")) {
2120             &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2121         }
2122          $uquery = "update accountlines set accounttype='F' ";
2123          if($outstanding  >= 0 && $amt >=0) {
2124             $uquery .= ", amount = ? , amountoutstanding=? ";
2125             unshift @bind, ($amt, $outstanding) ;
2126         }
2127     } else {
2128         $uquery = "update accountlines set accounttype='F' ";
2129     }
2130     $uquery .= " where (accountlines_id = ?)";
2131     my $usth = $dbh->prepare($uquery);
2132     return $usth->execute(@bind);
2133 }
2134
2135 =head2 _FixAccountForLostAndReturned
2136
2137   &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2138
2139 Calculates the charge for a book lost and returned.
2140
2141 Internal function, not exported, called only by AddReturn.
2142
2143 FIXME: This function reflects how inscrutable fines logic is.  Fix both.
2144 FIXME: Give a positive return value on success.  It might be the $borrowernumber who received credit, or the amount forgiven.
2145
2146 =cut
2147
2148 sub _FixAccountForLostAndReturned {
2149     my $itemnumber     = shift or return;
2150     my $borrowernumber = @_ ? shift : undef;
2151     my $item_id        = @_ ? shift : $itemnumber;  # Send the barcode if you want that logged in the description
2152     my $dbh = C4::Context->dbh;
2153     # check for charge made for lost book
2154     my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2155     $sth->execute($itemnumber);
2156     my $data = $sth->fetchrow_hashref;
2157     $data or return;    # bail if there is nothing to do
2158     $data->{accounttype} eq 'W' and return;    # Written off
2159
2160     # writeoff this amount
2161     my $offset;
2162     my $amount = $data->{'amount'};
2163     my $acctno = $data->{'accountno'};
2164     my $amountleft;                                             # Starts off undef/zero.
2165     if ($data->{'amountoutstanding'} == $amount) {
2166         $offset     = $data->{'amount'};
2167         $amountleft = 0;                                        # Hey, it's zero here, too.
2168     } else {
2169         $offset     = $amount - $data->{'amountoutstanding'};   # Um, isn't this the same as ZERO?  We just tested those two things are ==
2170         $amountleft = $data->{'amountoutstanding'} - $amount;   # Um, isn't this the same as ZERO?  We just tested those two things are ==
2171     }
2172     my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2173         WHERE (accountlines_id = ?)");
2174     $usth->execute($data->{'accountlines_id'});      # We might be adjusting an account for some OTHER borrowernumber now.  Not the one we passed in.
2175     #check if any credit is left if so writeoff other accounts
2176     my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2177     $amountleft *= -1 if ($amountleft < 0);
2178     if ($amountleft > 0) {
2179         my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2180                             AND (amountoutstanding >0) ORDER BY date");     # might want to order by amountoustanding ASC (pay smallest first)
2181         $msth->execute($data->{'borrowernumber'});
2182         # offset transactions
2183         my $newamtos;
2184         my $accdata;
2185         while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2186             if ($accdata->{'amountoutstanding'} < $amountleft) {
2187                 $newamtos = 0;
2188                 $amountleft -= $accdata->{'amountoutstanding'};
2189             }  else {
2190                 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2191                 $amountleft = 0;
2192             }
2193             my $thisacct = $accdata->{'accountlines_id'};
2194             # FIXME: move prepares outside while loop!
2195             my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2196                     WHERE (accountlines_id = ?)");
2197             $usth->execute($newamtos,$thisacct);
2198             $usth = $dbh->prepare("INSERT INTO accountoffsets
2199                 (borrowernumber, accountno, offsetaccount,  offsetamount)
2200                 VALUES
2201                 (?,?,?,?)");
2202             $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2203         }
2204     }
2205     $amountleft *= -1 if ($amountleft > 0);
2206     my $desc = "Item Returned " . $item_id;
2207     $usth = $dbh->prepare("INSERT INTO accountlines
2208         (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2209         VALUES (?,?,now(),?,?,'CR',?)");
2210     $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2211     if ($borrowernumber) {
2212         # FIXME: same as query above.  use 1 sth for both
2213         $usth = $dbh->prepare("INSERT INTO accountoffsets
2214             (borrowernumber, accountno, offsetaccount,  offsetamount)
2215             VALUES (?,?,?,?)");
2216         $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2217     }
2218     ModItem({ paidfor => '' }, undef, $itemnumber);
2219     return;
2220 }
2221
2222 =head2 _GetCircControlBranch
2223
2224    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2225
2226 Internal function : 
2227
2228 Return the library code to be used to determine which circulation
2229 policy applies to a transaction.  Looks up the CircControl and
2230 HomeOrHoldingBranch system preferences.
2231
2232 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2233
2234 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2235
2236 =cut
2237
2238 sub _GetCircControlBranch {
2239     my ($item, $borrower) = @_;
2240     my $circcontrol = C4::Context->preference('CircControl');
2241     my $branch;
2242
2243     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2244         $branch= C4::Context->userenv->{'branch'};
2245     } elsif ($circcontrol eq 'PatronLibrary') {
2246         $branch=$borrower->{branchcode};
2247     } else {
2248         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2249         $branch = $item->{$branchfield};
2250         # default to item home branch if holdingbranch is used
2251         # and is not defined
2252         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2253             $branch = $item->{homebranch};
2254         }
2255     }
2256     return $branch;
2257 }
2258
2259
2260
2261
2262
2263
2264 =head2 GetItemIssue
2265
2266   $issue = &GetItemIssue($itemnumber);
2267
2268 Returns patron currently having a book, or undef if not checked out.
2269
2270 C<$itemnumber> is the itemnumber.
2271
2272 C<$issue> is a hashref of the row from the issues table.
2273
2274 =cut
2275
2276 sub GetItemIssue {
2277     my ($itemnumber) = @_;
2278     return unless $itemnumber;
2279     my $sth = C4::Context->dbh->prepare(
2280         "SELECT items.*, issues.*
2281         FROM issues
2282         LEFT JOIN items ON issues.itemnumber=items.itemnumber
2283         WHERE issues.itemnumber=?");
2284     $sth->execute($itemnumber);
2285     my $data = $sth->fetchrow_hashref;
2286     return unless $data;
2287     $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql');
2288     $data->{issuedate}->truncate(to => 'minute');
2289     $data->{date_due} = dt_from_string($data->{date_due}, 'sql');
2290     $data->{date_due}->truncate(to => 'minute');
2291     my $dt = DateTime->now( time_zone => C4::Context->tz)->truncate( to => 'minute');
2292     $data->{'overdue'} = DateTime->compare($data->{'date_due'}, $dt ) == -1 ? 1 : 0;
2293     return $data;
2294 }
2295
2296 =head2 GetOpenIssue
2297
2298   $issue = GetOpenIssue( $itemnumber );
2299
2300 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2301
2302 C<$itemnumber> is the item's itemnumber
2303
2304 Returns a hashref
2305
2306 =cut
2307
2308 sub GetOpenIssue {
2309   my ( $itemnumber ) = @_;
2310   return unless $itemnumber;
2311   my $dbh = C4::Context->dbh;  
2312   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2313   $sth->execute( $itemnumber );
2314   return $sth->fetchrow_hashref();
2315
2316 }
2317
2318 =head2 GetIssues
2319
2320     $issues = GetIssues({});    # return all issues!
2321     $issues = GetIssues({ borrowernumber => $borrowernumber, biblionumber => $biblionumber });
2322
2323 Returns all pending issues that match given criteria.
2324 Returns a arrayref or undef if an error occurs.
2325
2326 Allowed criteria are:
2327
2328 =over 2
2329
2330 =item * borrowernumber
2331
2332 =item * biblionumber
2333
2334 =item * itemnumber
2335
2336 =back
2337
2338 =cut
2339
2340 sub GetIssues {
2341     my ($criteria) = @_;
2342
2343     # Build filters
2344     my @filters;
2345     my @allowed = qw(borrowernumber biblionumber itemnumber);
2346     foreach (@allowed) {
2347         if (defined $criteria->{$_}) {
2348             push @filters, {
2349                 field => $_,
2350                 value => $criteria->{$_},
2351             };
2352         }
2353     }
2354
2355     # Do we need to join other tables ?
2356     my %join;
2357     if (defined $criteria->{biblionumber}) {
2358         $join{items} = 1;
2359     }
2360
2361     # Build SQL query
2362     my $where = '';
2363     if (@filters) {
2364         $where = "WHERE " . join(' AND ', map { "$_->{field} = ?" } @filters);
2365     }
2366     my $query = q{
2367         SELECT issues.*
2368         FROM issues
2369     };
2370     if (defined $join{items}) {
2371         $query .= q{
2372             LEFT JOIN items ON (issues.itemnumber = items.itemnumber)
2373         };
2374     }
2375     $query .= $where;
2376
2377     # Execute SQL query
2378     my $dbh = C4::Context->dbh;
2379     my $sth = $dbh->prepare($query);
2380     my $rv = $sth->execute(map { $_->{value} } @filters);
2381
2382     return $rv ? $sth->fetchall_arrayref({}) : undef;
2383 }
2384
2385 =head2 GetItemIssues
2386
2387   $issues = &GetItemIssues($itemnumber, $history);
2388
2389 Returns patrons that have issued a book
2390
2391 C<$itemnumber> is the itemnumber
2392 C<$history> is false if you just want the current "issuer" (if any)
2393 and true if you want issues history from old_issues also.
2394
2395 Returns reference to an array of hashes
2396
2397 =cut
2398
2399 sub GetItemIssues {
2400     my ( $itemnumber, $history ) = @_;
2401     
2402     my $today = DateTime->now( time_zome => C4::Context->tz);  # get today date
2403     $today->truncate( to => 'minute' );
2404     my $sql = "SELECT * FROM issues
2405               JOIN borrowers USING (borrowernumber)
2406               JOIN items     USING (itemnumber)
2407               WHERE issues.itemnumber = ? ";
2408     if ($history) {
2409         $sql .= "UNION ALL
2410                  SELECT * FROM old_issues
2411                  LEFT JOIN borrowers USING (borrowernumber)
2412                  JOIN items USING (itemnumber)
2413                  WHERE old_issues.itemnumber = ? ";
2414     }
2415     $sql .= "ORDER BY date_due DESC";
2416     my $sth = C4::Context->dbh->prepare($sql);
2417     if ($history) {
2418         $sth->execute($itemnumber, $itemnumber);
2419     } else {
2420         $sth->execute($itemnumber);
2421     }
2422     my $results = $sth->fetchall_arrayref({});
2423     foreach (@$results) {
2424         my $date_due = dt_from_string($_->{date_due},'sql');
2425         $date_due->truncate( to => 'minute' );
2426
2427         $_->{overdue} = (DateTime->compare($date_due, $today) == -1) ? 1 : 0;
2428     }
2429     return $results;
2430 }
2431
2432 =head2 GetBiblioIssues
2433
2434   $issues = GetBiblioIssues($biblionumber);
2435
2436 this function get all issues from a biblionumber.
2437
2438 Return:
2439 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2440 tables issues and the firstname,surname & cardnumber from borrowers.
2441
2442 =cut
2443
2444 sub GetBiblioIssues {
2445     my $biblionumber = shift;
2446     return unless $biblionumber;
2447     my $dbh   = C4::Context->dbh;
2448     my $query = "
2449         SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2450         FROM issues
2451             LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2452             LEFT JOIN items ON issues.itemnumber = items.itemnumber
2453             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2454             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2455         WHERE biblio.biblionumber = ?
2456         UNION ALL
2457         SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2458         FROM old_issues
2459             LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2460             LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2461             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2462             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2463         WHERE biblio.biblionumber = ?
2464         ORDER BY timestamp
2465     ";
2466     my $sth = $dbh->prepare($query);
2467     $sth->execute($biblionumber, $biblionumber);
2468
2469     my @issues;
2470     while ( my $data = $sth->fetchrow_hashref ) {
2471         push @issues, $data;
2472     }
2473     return \@issues;
2474 }
2475
2476 =head2 GetUpcomingDueIssues
2477
2478   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2479
2480 =cut
2481
2482 sub GetUpcomingDueIssues {
2483     my $params = shift;
2484
2485     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2486     my $dbh = C4::Context->dbh;
2487
2488     my $statement = <<END_SQL;
2489 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2490 FROM issues 
2491 LEFT JOIN items USING (itemnumber)
2492 LEFT OUTER JOIN branches USING (branchcode)
2493 WHERE returndate is NULL
2494 HAVING days_until_due >= 0 AND days_until_due <= ?
2495 END_SQL
2496
2497     my @bind_parameters = ( $params->{'days_in_advance'} );
2498     
2499     my $sth = $dbh->prepare( $statement );
2500     $sth->execute( @bind_parameters );
2501     my $upcoming_dues = $sth->fetchall_arrayref({});
2502
2503     return $upcoming_dues;
2504 }
2505
2506 =head2 CanBookBeRenewed
2507
2508   ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2509
2510 Find out whether a borrowed item may be renewed.
2511
2512 C<$borrowernumber> is the borrower number of the patron who currently
2513 has the item on loan.
2514
2515 C<$itemnumber> is the number of the item to renew.
2516
2517 C<$override_limit>, if supplied with a true value, causes
2518 the limit on the number of times that the loan can be renewed
2519 (as controlled by the item type) to be ignored.
2520
2521 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2522 item must currently be on loan to the specified borrower; renewals
2523 must be allowed for the item's type; and the borrower must not have
2524 already renewed the loan. $error will contain the reason the renewal can not proceed
2525
2526 =cut
2527
2528 sub CanBookBeRenewed {
2529     my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2530
2531     my $dbh       = C4::Context->dbh;
2532     my $renews    = 1;
2533     my $renewokay = 1;
2534     my $error;
2535
2536     my $item      = GetItem($itemnumber)      or return ( 0, 'no_item' );
2537     my $itemissue = GetItemIssue($itemnumber) or return ( 0, 'no_checkout' );
2538
2539     $borrowernumber ||= $itemissue->{borrowernumber};
2540     my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber )
2541       or return;
2542
2543     my $branchcode  = _GetCircControlBranch($item, $borrower);
2544
2545     my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2546
2547     if ( $issuingrule->{norenewalbefore} ) {
2548
2549         # Get current time and add norenewalbefore. If this is smaller than date_due, it's too soon for renewal.
2550         if (
2551             DateTime->now( time_zone => C4::Context->tz() )->add(
2552                 $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore}
2553             ) < $itemissue->{date_due}
2554         )
2555         {
2556             $renewokay = 0;
2557             $error     = "too_soon";
2558         }
2559     }
2560
2561     if ( $issuingrule->{renewalsallowed} <= $itemissue->{renewals} ) {
2562         $renewokay = 0;
2563         $error = "too_many";
2564     }
2565
2566     if ( $override_limit ) {
2567         $renewokay = 1;
2568         $error     = undef;
2569     }
2570
2571     my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves( $itemnumber );
2572
2573     if ( $resfound ) { # '' when no hold was found
2574         $renewokay = 0;
2575         $error = "on_reserve";
2576     }
2577
2578     return ( $renewokay, $error );
2579 }
2580
2581 =head2 AddRenewal
2582
2583   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2584
2585 Renews a loan.
2586
2587 C<$borrowernumber> is the borrower number of the patron who currently
2588 has the item.
2589
2590 C<$itemnumber> is the number of the item to renew.
2591
2592 C<$branch> is the library where the renewal took place (if any).
2593            The library that controls the circ policies for the renewal is retrieved from the issues record.
2594
2595 C<$datedue> can be a C4::Dates object used to set the due date.
2596
2597 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2598 this parameter is not supplied, lastreneweddate is set to the current date.
2599
2600 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2601 from the book's item type.
2602
2603 =cut
2604
2605 sub AddRenewal {
2606     my $borrowernumber  = shift;
2607     my $itemnumber      = shift or return;
2608     my $branch          = shift;
2609     my $datedue         = shift;
2610     my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2611
2612     my $item   = GetItem($itemnumber) or return;
2613     my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2614
2615     my $dbh = C4::Context->dbh;
2616
2617     # Find the issues record for this book
2618     my $sth =
2619       $dbh->prepare("SELECT * FROM issues WHERE itemnumber = ?");
2620     $sth->execute( $itemnumber );
2621     my $issuedata = $sth->fetchrow_hashref;
2622
2623     return unless ( $issuedata );
2624
2625     $borrowernumber ||= $issuedata->{borrowernumber};
2626
2627     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2628         carp 'Invalid date passed to AddRenewal.';
2629         return;
2630     }
2631
2632     # If the due date wasn't specified, calculate it by adding the
2633     # book's loan length to today's date or the current due date
2634     # based on the value of the RenewalPeriodBase syspref.
2635     unless ($datedue) {
2636
2637         my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2638         my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2639
2640         $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2641                                         dt_from_string( $issuedata->{date_due} ) :
2642                                         DateTime->now( time_zone => C4::Context->tz());
2643         $datedue =  CalcDateDue($datedue, $itemtype, $issuedata->{'branchcode'}, $borrower, 'is a renewal');
2644     }
2645
2646     # Update the issues record to have the new due date, and a new count
2647     # of how many times it has been renewed.
2648     my $renews = $issuedata->{'renewals'} + 1;
2649     $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2650                             WHERE borrowernumber=? 
2651                             AND itemnumber=?"
2652     );
2653
2654     $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2655
2656     # Update the renewal count on the item, and tell zebra to reindex
2657     $renews = $biblio->{'renewals'} + 1;
2658     ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2659
2660     # Charge a new rental fee, if applicable?
2661     my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2662     if ( $charge > 0 ) {
2663         my $accountno = getnextacctno( $borrowernumber );
2664         my $item = GetBiblioFromItemNumber($itemnumber);
2665         my $manager_id = 0;
2666         $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; 
2667         $sth = $dbh->prepare(
2668                 "INSERT INTO accountlines
2669                     (date, borrowernumber, accountno, amount, manager_id,
2670                     description,accounttype, amountoutstanding, itemnumber)
2671                     VALUES (now(),?,?,?,?,?,?,?,?)"
2672         );
2673         $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2674             "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2675             'Rent', $charge, $itemnumber );
2676     }
2677
2678     # Send a renewal slip according to checkout alert preferencei
2679     if ( C4::Context->preference('RenewalSendNotice') eq '1') {
2680         my $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 );
2681         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2682         my %conditions = (
2683                 branchcode   => $branch,
2684                 categorycode => $borrower->{categorycode},
2685                 item_type    => $item->{itype},
2686                 notification => 'CHECKOUT',
2687         );
2688         if ($circulation_alert->is_enabled_for(\%conditions)) {
2689                 SendCirculationAlert({
2690                         type     => 'RENEWAL',
2691                         item     => $item,
2692                 borrower => $borrower,
2693                 branch   => $branch,
2694                 });
2695         }
2696     }
2697
2698     # Remove any OVERDUES related debarment if the borrower has no overdues
2699     my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
2700     if ( $borrowernumber
2701       && $borrower->{'debarred'}
2702       && !HasOverdues( $borrowernumber )
2703       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2704     ) {
2705         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2706     }
2707
2708     # Log the renewal
2709     UpdateStats( $branch, 'renew', $charge, '', $itemnumber, $item->{itype}, $borrowernumber, undef, $item->{'ccode'});
2710         return $datedue;
2711 }
2712
2713 sub GetRenewCount {
2714     # check renewal status
2715     my ( $bornum, $itemno ) = @_;
2716     my $dbh           = C4::Context->dbh;
2717     my $renewcount    = 0;
2718     my $renewsallowed = 0;
2719     my $renewsleft    = 0;
2720
2721     my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2722     my $item     = GetItem($itemno); 
2723
2724     # Look in the issues table for this item, lent to this borrower,
2725     # and not yet returned.
2726
2727     # FIXME - I think this function could be redone to use only one SQL call.
2728     my $sth = $dbh->prepare(
2729         "select * from issues
2730                                 where (borrowernumber = ?)
2731                                 and (itemnumber = ?)"
2732     );
2733     $sth->execute( $bornum, $itemno );
2734     my $data = $sth->fetchrow_hashref;
2735     $renewcount = $data->{'renewals'} if $data->{'renewals'};
2736     # $item and $borrower should be calculated
2737     my $branchcode = _GetCircControlBranch($item, $borrower);
2738     
2739     my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2740     
2741     $renewsallowed = $issuingrule->{'renewalsallowed'};
2742     $renewsleft    = $renewsallowed - $renewcount;
2743     if($renewsleft < 0){ $renewsleft = 0; }
2744     return ( $renewcount, $renewsallowed, $renewsleft );
2745 }
2746
2747 =head2 GetSoonestRenewDate
2748
2749   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
2750
2751 Find out the soonest possible renew date of a borrowed item.
2752
2753 C<$borrowernumber> is the borrower number of the patron who currently
2754 has the item on loan.
2755
2756 C<$itemnumber> is the number of the item to renew.
2757
2758 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
2759 renew date, based on the value "No renewal before" of the applicable
2760 issuing rule. Returns the current date if the item can already be
2761 renewed, and returns undefined if the borrower, loan, or item
2762 cannot be found.
2763
2764 =cut
2765
2766 sub GetSoonestRenewDate {
2767     my ( $borrowernumber, $itemnumber ) = @_;
2768
2769     my $dbh = C4::Context->dbh;
2770
2771     my $item      = GetItem($itemnumber)      or return;
2772     my $itemissue = GetItemIssue($itemnumber) or return;
2773
2774     $borrowernumber ||= $itemissue->{borrowernumber};
2775     my $borrower = C4::Members::GetMemberDetails($borrowernumber)
2776       or return;
2777
2778     my $branchcode = _GetCircControlBranch( $item, $borrower );
2779     my $issuingrule =
2780       GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2781
2782     my $now = DateTime->now( time_zone => C4::Context->tz() );
2783
2784     if ( $issuingrule->{norenewalbefore} ) {
2785         my $soonestrenewal =
2786           $itemissue->{date_due}->subtract(
2787             $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} );
2788
2789         $soonestrenewal = $now > $soonestrenewal ? $now : $soonestrenewal;
2790         return $soonestrenewal;
2791     }
2792     return $now;
2793 }
2794
2795 =head2 GetIssuingCharges
2796
2797   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
2798
2799 Calculate how much it would cost for a given patron to borrow a given
2800 item, including any applicable discounts.
2801
2802 C<$itemnumber> is the item number of item the patron wishes to borrow.
2803
2804 C<$borrowernumber> is the patron's borrower number.
2805
2806 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
2807 and C<$item_type> is the code for the item's item type (e.g., C<VID>
2808 if it's a video).
2809
2810 =cut
2811
2812 sub GetIssuingCharges {
2813
2814     # calculate charges due
2815     my ( $itemnumber, $borrowernumber ) = @_;
2816     my $charge = 0;
2817     my $dbh    = C4::Context->dbh;
2818     my $item_type;
2819
2820     # Get the book's item type and rental charge (via its biblioitem).
2821     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
2822         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
2823     $charge_query .= (C4::Context->preference('item-level_itypes'))
2824         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
2825         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
2826
2827     $charge_query .= ' WHERE items.itemnumber =?';
2828
2829     my $sth = $dbh->prepare($charge_query);
2830     $sth->execute($itemnumber);
2831     if ( my $item_data = $sth->fetchrow_hashref ) {
2832         $item_type = $item_data->{itemtype};
2833         $charge    = $item_data->{rentalcharge};
2834         my $branch = C4::Branch::mybranch();
2835         my $discount_query = q|SELECT rentaldiscount,
2836             issuingrules.itemtype, issuingrules.branchcode
2837             FROM borrowers
2838             LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
2839             WHERE borrowers.borrowernumber = ?
2840             AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2841             AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
2842         my $discount_sth = $dbh->prepare($discount_query);
2843         $discount_sth->execute( $borrowernumber, $item_type, $branch );
2844         my $discount_rules = $discount_sth->fetchall_arrayref({});
2845         if (@{$discount_rules}) {
2846             # We may have multiple rules so get the most specific
2847             my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
2848             $charge = ( $charge * ( 100 - $discount ) ) / 100;
2849         }
2850     }
2851
2852     return ( $charge, $item_type );
2853 }
2854
2855 # Select most appropriate discount rule from those returned
2856 sub _get_discount_from_rule {
2857     my ($rules_ref, $branch, $itemtype) = @_;
2858     my $discount;
2859
2860     if (@{$rules_ref} == 1) { # only 1 applicable rule use it
2861         $discount = $rules_ref->[0]->{rentaldiscount};
2862         return (defined $discount) ? $discount : 0;
2863     }
2864     # could have up to 4 does one match $branch and $itemtype
2865     my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
2866     if (@d) {
2867         $discount = $d[0]->{rentaldiscount};
2868         return (defined $discount) ? $discount : 0;
2869     }
2870     # do we have item type + all branches
2871     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
2872     if (@d) {
2873         $discount = $d[0]->{rentaldiscount};
2874         return (defined $discount) ? $discount : 0;
2875     }
2876     # do we all item types + this branch
2877     @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
2878     if (@d) {
2879         $discount = $d[0]->{rentaldiscount};
2880         return (defined $discount) ? $discount : 0;
2881     }
2882     # so all and all (surely we wont get here)
2883     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
2884     if (@d) {
2885         $discount = $d[0]->{rentaldiscount};
2886         return (defined $discount) ? $discount : 0;
2887     }
2888     # none of the above
2889     return 0;
2890 }
2891
2892 =head2 AddIssuingCharge
2893
2894   &AddIssuingCharge( $itemno, $borrowernumber, $charge )
2895
2896 =cut
2897
2898 sub AddIssuingCharge {
2899     my ( $itemnumber, $borrowernumber, $charge ) = @_;
2900     my $dbh = C4::Context->dbh;
2901     my $nextaccntno = getnextacctno( $borrowernumber );
2902     my $manager_id = 0;
2903     $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2904     my $query ="
2905         INSERT INTO accountlines
2906             (borrowernumber, itemnumber, accountno,
2907             date, amount, description, accounttype,
2908             amountoutstanding, manager_id)
2909         VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
2910     ";
2911     my $sth = $dbh->prepare($query);
2912     $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
2913 }
2914
2915 =head2 GetTransfers
2916
2917   GetTransfers($itemnumber);
2918
2919 =cut
2920
2921 sub GetTransfers {
2922     my ($itemnumber) = @_;
2923
2924     my $dbh = C4::Context->dbh;
2925
2926     my $query = '
2927         SELECT datesent,
2928                frombranch,
2929                tobranch
2930         FROM branchtransfers
2931         WHERE itemnumber = ?
2932           AND datearrived IS NULL
2933         ';
2934     my $sth = $dbh->prepare($query);
2935     $sth->execute($itemnumber);
2936     my @row = $sth->fetchrow_array();
2937     return @row;
2938 }
2939
2940 =head2 GetTransfersFromTo
2941
2942   @results = GetTransfersFromTo($frombranch,$tobranch);
2943
2944 Returns the list of pending transfers between $from and $to branch
2945
2946 =cut
2947
2948 sub GetTransfersFromTo {
2949     my ( $frombranch, $tobranch ) = @_;
2950     return unless ( $frombranch && $tobranch );
2951     my $dbh   = C4::Context->dbh;
2952     my $query = "
2953         SELECT itemnumber,datesent,frombranch
2954         FROM   branchtransfers
2955         WHERE  frombranch=?
2956           AND  tobranch=?
2957           AND datearrived IS NULL
2958     ";
2959     my $sth = $dbh->prepare($query);
2960     $sth->execute( $frombranch, $tobranch );
2961     my @gettransfers;
2962
2963     while ( my $data = $sth->fetchrow_hashref ) {
2964         push @gettransfers, $data;
2965     }
2966     return (@gettransfers);
2967 }
2968
2969 =head2 DeleteTransfer
2970
2971   &DeleteTransfer($itemnumber);
2972
2973 =cut
2974
2975 sub DeleteTransfer {
2976     my ($itemnumber) = @_;
2977     return unless $itemnumber;
2978     my $dbh          = C4::Context->dbh;
2979     my $sth          = $dbh->prepare(
2980         "DELETE FROM branchtransfers
2981          WHERE itemnumber=?
2982          AND datearrived IS NULL "
2983     );
2984     return $sth->execute($itemnumber);
2985 }
2986
2987 =head2 AnonymiseIssueHistory
2988
2989   ($rows,$err_history_not_deleted) = AnonymiseIssueHistory($date,$borrowernumber)
2990
2991 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
2992 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
2993
2994 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
2995 setting (force delete).
2996
2997 return the number of affected rows and a value that evaluates to true if an error occurred deleting the history.
2998
2999 =cut
3000
3001 sub AnonymiseIssueHistory {
3002     my $date           = shift;
3003     my $borrowernumber = shift;
3004     my $dbh            = C4::Context->dbh;
3005     my $query          = "
3006         UPDATE old_issues
3007         SET    borrowernumber = ?
3008         WHERE  returndate < ?
3009           AND borrowernumber IS NOT NULL
3010     ";
3011
3012     # The default of 0 does not work due to foreign key constraints
3013     # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
3014     my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
3015     my @bind_params = ($anonymouspatron, $date);
3016     if (defined $borrowernumber) {
3017        $query .= " AND borrowernumber = ?";
3018        push @bind_params, $borrowernumber;
3019     } else {
3020        $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
3021     }
3022     my $sth = $dbh->prepare($query);
3023     $sth->execute(@bind_params);
3024     my $anonymisation_err = $dbh->err;
3025     my $rows_affected = $sth->rows;  ### doublecheck row count return function
3026     return ($rows_affected, $anonymisation_err);
3027 }
3028
3029 =head2 SendCirculationAlert
3030
3031 Send out a C<check-in> or C<checkout> alert using the messaging system.
3032
3033 B<Parameters>:
3034
3035 =over 4
3036
3037 =item type
3038
3039 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3040
3041 =item item
3042
3043 Hashref of information about the item being checked in or out.
3044
3045 =item borrower
3046
3047 Hashref of information about the borrower of the item.
3048
3049 =item branch
3050
3051 The branchcode from where the checkout or check-in took place.
3052
3053 =back
3054
3055 B<Example>:
3056
3057     SendCirculationAlert({
3058         type     => 'CHECKOUT',
3059         item     => $item,
3060         borrower => $borrower,
3061         branch   => $branch,
3062     });
3063
3064 =cut
3065
3066 sub SendCirculationAlert {
3067     my ($opts) = @_;
3068     my ($type, $item, $borrower, $branch) =
3069         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3070     my %message_name = (
3071         CHECKIN  => 'Item_Check_in',
3072         CHECKOUT => 'Item_Checkout',
3073         RENEWAL  => 'Item_Checkout',
3074     );
3075     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3076         borrowernumber => $borrower->{borrowernumber},
3077         message_name   => $message_name{$type},
3078     });
3079     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3080     my $letter =  C4::Letters::GetPreparedLetter (
3081         module => 'circulation',
3082         letter_code => $type,
3083         branchcode => $branch,
3084         tables => {
3085             $issues_table => $item->{itemnumber},
3086             'items'       => $item->{itemnumber},
3087             'biblio'      => $item->{biblionumber},
3088             'biblioitems' => $item->{biblionumber},
3089             'borrowers'   => $borrower,
3090             'branches'    => $branch,
3091         }
3092     ) or return;
3093
3094     my @transports = keys %{ $borrower_preferences->{transports} };
3095     # warn "no transports" unless @transports;
3096     for (@transports) {
3097         # warn "transport: $_";
3098         my $message = C4::Message->find_last_message($borrower, $type, $_);
3099         if (!$message) {
3100             #warn "create new message";
3101             C4::Message->enqueue($letter, $borrower, $_);
3102         } else {
3103             #warn "append to old message";
3104             $message->append($letter);
3105             $message->update;
3106         }
3107     }
3108
3109     return $letter;
3110 }
3111
3112 =head2 updateWrongTransfer
3113
3114   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3115
3116 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 
3117
3118 =cut
3119
3120 sub updateWrongTransfer {
3121         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3122         my $dbh = C4::Context->dbh;     
3123 # first step validate the actual line of transfert .
3124         my $sth =
3125                 $dbh->prepare(
3126                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3127                 );
3128                 $sth->execute($FromLibrary,$itemNumber);
3129
3130 # second step create a new line of branchtransfer to the right location .
3131         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3132
3133 #third step changing holdingbranch of item
3134         UpdateHoldingbranch($FromLibrary,$itemNumber);
3135 }
3136
3137 =head2 UpdateHoldingbranch
3138
3139   $items = UpdateHoldingbranch($branch,$itmenumber);
3140
3141 Simple methode for updating hodlingbranch in items BDD line
3142
3143 =cut
3144
3145 sub UpdateHoldingbranch {
3146         my ( $branch,$itemnumber ) = @_;
3147     ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3148 }
3149
3150 =head2 CalcDateDue
3151
3152 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3153
3154 this function calculates the due date given the start date and configured circulation rules,
3155 checking against the holidays calendar as per the 'useDaysMode' syspref.
3156 C<$startdate>   = C4::Dates object representing start date of loan period (assumed to be today)
3157 C<$itemtype>  = itemtype code of item in question
3158 C<$branch>  = location whose calendar to use
3159 C<$borrower> = Borrower object
3160 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3161
3162 =cut
3163
3164 sub CalcDateDue {
3165     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3166
3167     $isrenewal ||= 0;
3168
3169     # loanlength now a href
3170     my $loanlength =
3171             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3172
3173     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3174             ? qq{renewalperiod}
3175             : qq{issuelength};
3176
3177     my $datedue;
3178     if ( $startdate ) {
3179         if (ref $startdate ne 'DateTime' ) {
3180             $datedue = dt_from_string($datedue);
3181         } else {
3182             $datedue = $startdate->clone;
3183         }
3184     } else {
3185         $datedue =
3186           DateTime->now( time_zone => C4::Context->tz() )
3187           ->truncate( to => 'minute' );
3188     }
3189
3190
3191     # calculate the datedue as normal
3192     if ( C4::Context->preference('useDaysMode') eq 'Days' )
3193     {    # ignoring calendar
3194         if ( $loanlength->{lengthunit} eq 'hours' ) {
3195             $datedue->add( hours => $loanlength->{$length_key} );
3196         } else {    # days
3197             $datedue->add( days => $loanlength->{$length_key} );
3198             $datedue->set_hour(23);
3199             $datedue->set_minute(59);
3200         }
3201     } else {
3202         my $dur;
3203         if ($loanlength->{lengthunit} eq 'hours') {
3204             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3205         }
3206         else { # days
3207             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3208         }
3209         my $calendar = Koha::Calendar->new( branchcode => $branch );
3210         $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3211         if ($loanlength->{lengthunit} eq 'days') {
3212             $datedue->set_hour(23);
3213             $datedue->set_minute(59);
3214         }
3215     }
3216
3217     # if Hard Due Dates are used, retreive them and apply as necessary
3218     my ( $hardduedate, $hardduedatecompare ) =
3219       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3220     if ($hardduedate) {    # hardduedates are currently dates
3221         $hardduedate->truncate( to => 'minute' );
3222         $hardduedate->set_hour(23);
3223         $hardduedate->set_minute(59);
3224         my $cmp = DateTime->compare( $hardduedate, $datedue );
3225
3226 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3227 # if the calculated date is before the 'after' Hard Due Date (floor), override
3228 # if the hard due date is set to 'exactly', overrride
3229         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3230             $datedue = $hardduedate->clone;
3231         }
3232
3233         # in all other cases, keep the date due as it is
3234
3235     }
3236
3237     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3238     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3239         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso' );
3240         $expiry_dt->set( hour => 23, minute => 59);
3241         if ( DateTime->compare( $datedue, $expiry_dt ) == 1 ) {
3242             $datedue = $expiry_dt->clone;
3243         }
3244     }
3245
3246     return $datedue;
3247 }
3248
3249
3250 =head2 CheckRepeatableHolidays
3251
3252   $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
3253
3254 This function checks if the date due is a repeatable holiday
3255
3256 C<$date_due>   = returndate calculate with no day check
3257 C<$itemnumber>  = itemnumber
3258 C<$branchcode>  = localisation of issue 
3259
3260 =cut
3261
3262 sub CheckRepeatableHolidays{
3263 my($itemnumber,$week_day,$branchcode)=@_;
3264 my $dbh = C4::Context->dbh;
3265 my $query = qq|SELECT count(*)  
3266         FROM repeatable_holidays 
3267         WHERE branchcode=?
3268         AND weekday=?|;
3269 my $sth = $dbh->prepare($query);
3270 $sth->execute($branchcode,$week_day);
3271 my $result=$sth->fetchrow;
3272 return $result;
3273 }
3274
3275
3276 =head2 CheckSpecialHolidays
3277
3278   $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
3279
3280 This function check if the date is a special holiday
3281
3282 C<$years>   = the years of datedue
3283 C<$month>   = the month of datedue
3284 C<$day>     = the day of datedue
3285 C<$itemnumber>  = itemnumber
3286 C<$branchcode>  = localisation of issue 
3287
3288 =cut
3289
3290 sub CheckSpecialHolidays{
3291 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
3292 my $dbh = C4::Context->dbh;
3293 my $query=qq|SELECT count(*) 
3294              FROM `special_holidays`
3295              WHERE year=?
3296              AND month=?
3297              AND day=?
3298              AND branchcode=?
3299             |;
3300 my $sth = $dbh->prepare($query);
3301 $sth->execute($years,$month,$day,$branchcode);
3302 my $countspecial=$sth->fetchrow ;
3303 return $countspecial;
3304 }
3305
3306 =head2 CheckRepeatableSpecialHolidays
3307
3308   $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
3309
3310 This function check if the date is a repeatble special holidays
3311
3312 C<$month>   = the month of datedue
3313 C<$day>     = the day of datedue
3314 C<$itemnumber>  = itemnumber
3315 C<$branchcode>  = localisation of issue 
3316
3317 =cut
3318
3319 sub CheckRepeatableSpecialHolidays{
3320 my ($month,$day,$itemnumber,$branchcode) = @_;
3321 my $dbh = C4::Context->dbh;
3322 my $query=qq|SELECT count(*) 
3323              FROM `repeatable_holidays`
3324              WHERE month=?
3325              AND day=?
3326              AND branchcode=?
3327             |;
3328 my $sth = $dbh->prepare($query);
3329 $sth->execute($month,$day,$branchcode);
3330 my $countspecial=$sth->fetchrow ;
3331 return $countspecial;
3332 }
3333
3334
3335
3336 sub CheckValidBarcode{
3337 my ($barcode) = @_;
3338 my $dbh = C4::Context->dbh;
3339 my $query=qq|SELECT count(*) 
3340              FROM items 
3341              WHERE barcode=?
3342             |;
3343 my $sth = $dbh->prepare($query);
3344 $sth->execute($barcode);
3345 my $exist=$sth->fetchrow ;
3346 return $exist;
3347 }
3348
3349 =head2 IsBranchTransferAllowed
3350
3351   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3352
3353 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3354
3355 =cut
3356
3357 sub IsBranchTransferAllowed {
3358         my ( $toBranch, $fromBranch, $code ) = @_;
3359
3360         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3361         
3362         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3363         my $dbh = C4::Context->dbh;
3364             
3365         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3366         $sth->execute( $toBranch, $fromBranch, $code );
3367         my $limit = $sth->fetchrow_hashref();
3368                         
3369         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3370         if ( $limit->{'limitId'} ) {
3371                 return 0;
3372         } else {
3373                 return 1;
3374         }
3375 }                                                        
3376
3377 =head2 CreateBranchTransferLimit
3378
3379   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3380
3381 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3382
3383 =cut
3384
3385 sub CreateBranchTransferLimit {
3386    my ( $toBranch, $fromBranch, $code ) = @_;
3387    return unless defined($toBranch) && defined($fromBranch);
3388    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3389    
3390    my $dbh = C4::Context->dbh;
3391    
3392    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3393    return $sth->execute( $code, $toBranch, $fromBranch );
3394 }
3395
3396 =head2 DeleteBranchTransferLimits
3397
3398     my $result = DeleteBranchTransferLimits($frombranch);
3399
3400 Deletes all the library transfer limits for one library.  Returns the
3401 number of limits deleted, 0e0 if no limits were deleted, or undef if
3402 no arguments are supplied.
3403
3404 =cut
3405
3406 sub DeleteBranchTransferLimits {
3407     my $branch = shift;
3408     return unless defined $branch;
3409     my $dbh    = C4::Context->dbh;
3410     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3411     return $sth->execute($branch);
3412 }
3413
3414 sub ReturnLostItem{
3415     my ( $borrowernumber, $itemnum ) = @_;
3416
3417     MarkIssueReturned( $borrowernumber, $itemnum );
3418     my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3419     my $item = C4::Items::GetItem( $itemnum );
3420     my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3421     my @datearr = localtime(time);
3422     my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3423     my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3424     ModItem({ paidfor =>  $old_note."Paid for by $bor $date" }, undef, $itemnum);
3425 }
3426
3427
3428 sub LostItem{
3429     my ($itemnumber, $mark_returned) = @_;
3430
3431     my $dbh = C4::Context->dbh();
3432     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3433                            FROM issues 
3434                            JOIN items USING (itemnumber) 
3435                            JOIN biblio USING (biblionumber)
3436                            WHERE issues.itemnumber=?");
3437     $sth->execute($itemnumber);
3438     my $issues=$sth->fetchrow_hashref();
3439
3440     # If a borrower lost the item, add a replacement cost to the their record
3441     if ( my $borrowernumber = $issues->{borrowernumber} ){
3442         my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3443
3444         if (C4::Context->preference('WhenLostForgiveFine')){
3445             my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3446             defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3447         }
3448         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3449             C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3450             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3451             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3452         }
3453
3454         MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3455     }
3456 }
3457
3458 sub GetOfflineOperations {
3459     my $dbh = C4::Context->dbh;
3460     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3461     $sth->execute(C4::Context->userenv->{'branch'});
3462     my $results = $sth->fetchall_arrayref({});
3463     return $results;
3464 }
3465
3466 sub GetOfflineOperation {
3467     my $operationid = shift;
3468     return unless $operationid;
3469     my $dbh = C4::Context->dbh;
3470     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3471     $sth->execute( $operationid );
3472     return $sth->fetchrow_hashref;
3473 }
3474
3475 sub AddOfflineOperation {
3476     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3477     my $dbh = C4::Context->dbh;
3478     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3479     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3480     return "Added.";
3481 }
3482
3483 sub DeleteOfflineOperation {
3484     my $dbh = C4::Context->dbh;
3485     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3486     $sth->execute( shift );
3487     return "Deleted.";
3488 }
3489
3490 sub ProcessOfflineOperation {
3491     my $operation = shift;
3492
3493     my $report;
3494     if ( $operation->{action} eq 'return' ) {
3495         $report = ProcessOfflineReturn( $operation );
3496     } elsif ( $operation->{action} eq 'issue' ) {
3497         $report = ProcessOfflineIssue( $operation );
3498     } elsif ( $operation->{action} eq 'payment' ) {
3499         $report = ProcessOfflinePayment( $operation );
3500     }
3501
3502     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3503
3504     return $report;
3505 }
3506
3507 sub ProcessOfflineReturn {
3508     my $operation = shift;
3509
3510     my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3511
3512     if ( $itemnumber ) {
3513         my $issue = GetOpenIssue( $itemnumber );
3514         if ( $issue ) {
3515             MarkIssueReturned(
3516                 $issue->{borrowernumber},
3517                 $itemnumber,
3518                 undef,
3519                 $operation->{timestamp},
3520             );
3521             ModItem(
3522                 { renewals => 0, onloan => undef },
3523                 $issue->{'biblionumber'},
3524                 $itemnumber
3525             );
3526             return "Success.";
3527         } else {
3528             return "Item not issued.";
3529         }
3530     } else {
3531         return "Item not found.";
3532     }
3533 }
3534
3535 sub ProcessOfflineIssue {
3536     my $operation = shift;
3537
3538     my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3539
3540     if ( $borrower->{borrowernumber} ) {
3541         my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3542         unless ($itemnumber) {
3543             return "Barcode not found.";
3544         }
3545         my $issue = GetOpenIssue( $itemnumber );
3546
3547         if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3548             MarkIssueReturned(
3549                 $issue->{borrowernumber},
3550                 $itemnumber,
3551                 undef,
3552                 $operation->{timestamp},
3553             );
3554         }
3555         AddIssue(
3556             $borrower,
3557             $operation->{'barcode'},
3558             undef,
3559             1,
3560             $operation->{timestamp},
3561             undef,
3562         );
3563         return "Success.";
3564     } else {
3565         return "Borrower not found.";
3566     }
3567 }
3568
3569 sub ProcessOfflinePayment {
3570     my $operation = shift;
3571
3572     my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3573     my $amount = $operation->{amount};
3574
3575     recordpayment( $borrower->{borrowernumber}, $amount );
3576
3577     return "Success."
3578 }
3579
3580
3581 =head2 TransferSlip
3582
3583   TransferSlip($user_branch, $itemnumber, $to_branch)
3584
3585   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3586
3587 =cut
3588
3589 sub TransferSlip {
3590     my ($branch, $itemnumber, $to_branch) = @_;
3591
3592     my $item =  GetItem( $itemnumber )
3593       or return;
3594
3595     my $pulldate = C4::Dates->new();
3596
3597     return C4::Letters::GetPreparedLetter (
3598         module => 'circulation',
3599         letter_code => 'TRANSFERSLIP',
3600         branchcode => $branch,
3601         tables => {
3602             'branches'    => $to_branch,
3603             'biblio'      => $item->{biblionumber},
3604             'items'       => $item,
3605         },
3606     );
3607 }
3608
3609 =head2 CheckIfIssuedToPatron
3610
3611   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3612
3613   Return 1 if any record item is issued to patron, otherwise return 0
3614
3615 =cut
3616
3617 sub CheckIfIssuedToPatron {
3618     my ($borrowernumber, $biblionumber) = @_;
3619
3620     my $items = GetItemsByBiblioitemnumber($biblionumber);
3621
3622     foreach my $item (@{$items}) {
3623         return 1 if ($item->{borrowernumber} && $item->{borrowernumber} eq $borrowernumber);
3624     }
3625
3626     return;
3627 }
3628
3629 =head2 IsItemIssued
3630
3631   IsItemIssued( $itemnumber )
3632
3633   Return 1 if the item is on loan, otherwise return 0
3634
3635 =cut
3636
3637 sub IsItemIssued {
3638     my $itemnumber = shift;
3639     my $dbh = C4::Context->dbh;
3640     my $sth = $dbh->prepare(q{
3641         SELECT COUNT(*)
3642         FROM issues
3643         WHERE itemnumber = ?
3644     });
3645     $sth->execute($itemnumber);
3646     return $sth->fetchrow;
3647 }
3648
3649 sub GetAgeRestriction {
3650     my ($record_restrictions) = @_;
3651     my $markers = C4::Context->preference('AgeRestrictionMarker');
3652
3653     # Split $record_restrictions to something like FSK 16 or PEGI 6
3654     my @values = split ' ', uc($record_restrictions);
3655     return unless @values;
3656
3657     # Search first occurence of one of the markers
3658     my @markers = split /\|/, uc($markers);
3659     return unless @markers;
3660
3661     my $index            = 0;
3662     my $restriction_year = 0;
3663     for my $value (@values) {
3664         $index++;
3665         for my $marker (@markers) {
3666             $marker =~ s/^\s+//;    #remove leading spaces
3667             $marker =~ s/\s+$//;    #remove trailing spaces
3668             if ( $marker eq $value ) {
3669                 if ( $index <= $#values ) {
3670                     $restriction_year += $values[$index];
3671                 }
3672                 last;
3673             }
3674             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3675
3676                 # Perhaps it is something like "K16" (as in Finland)
3677                 $restriction_year += $1;
3678                 last;
3679             }
3680         }
3681         last if ( $restriction_year > 0 );
3682     }
3683
3684     return $restriction_year;
3685 }
3686
3687 1;
3688
3689 __END__
3690
3691 =head1 AUTHOR
3692
3693 Koha Development Team <http://koha-community.org/>
3694
3695 =cut
3696