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