Bug 14903: Remove C4::Dates from circ/circulation.pl (and more)
[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 Koha::DateUtils;
26 use C4::Context;
27 use C4::Stats;
28 use C4::Reserves;
29 use C4::Biblio;
30 use C4::Items;
31 use C4::Members;
32 use C4::Accounts;
33 use C4::ItemCirculationAlertPreference;
34 use C4::Message;
35 use C4::Debug;
36 use C4::Branch; # GetBranches
37 use C4::Log; # logaction
38 use C4::Koha qw(
39     GetAuthorisedValueByCode
40     GetAuthValCode
41     GetKohaAuthorisedValueLib
42 );
43 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
44 use C4::RotatingCollections qw(GetCollectionItemBranches);
45 use Algorithm::CheckDigits;
46
47 use Data::Dumper;
48 use Koha::DateUtils;
49 use Koha::Calendar;
50 use Koha::Borrower::Debarments;
51 use Koha::Database;
52 use Carp;
53 use List::MoreUtils qw( uniq );
54 use Date::Calc qw(
55   Today
56   Today_and_Now
57   Add_Delta_YM
58   Add_Delta_DHMS
59   Date_to_Days
60   Day_of_Week
61   Add_Delta_Days
62 );
63 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
64
65 BEGIN {
66         require Exporter;
67     $VERSION = 3.07.00.049;     # for version checking
68         @ISA    = qw(Exporter);
69
70         # FIXME subs that should probably be elsewhere
71         push @EXPORT, qw(
72                 &barcodedecode
73         &LostItem
74         &ReturnLostItem
75         &GetPendingOnSiteCheckouts
76         );
77
78         # subs to deal with issuing a book
79         push @EXPORT, qw(
80                 &CanBookBeIssued
81                 &CanBookBeRenewed
82                 &AddIssue
83                 &AddRenewal
84                 &GetRenewCount
85         &GetSoonestRenewDate
86                 &GetItemIssue
87                 &GetItemIssues
88                 &GetIssuingCharges
89                 &GetIssuingRule
90         &GetBranchBorrowerCircRule
91         &GetBranchItemRule
92                 &GetBiblioIssues
93                 &GetOpenIssue
94                 &AnonymiseIssueHistory
95         &CheckIfIssuedToPatron
96         &IsItemIssued
97         GetTopIssues
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, $duedate, $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<$duedates> is a DateTime 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'} = $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'} = $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 DateTime 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 DateTime 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
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     return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2799
2800     return ( 1, undef ) if $override_limit;
2801
2802     my $branchcode = _GetCircControlBranch( $item, $borrower );
2803     my $issuingrule =
2804       GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2805
2806     return ( 0, "too_many" )
2807       if $issuingrule->{renewalsallowed} <= $itemissue->{renewals};
2808
2809     my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2810     my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2811     my $restricted = Koha::Borrower::Debarments::IsDebarred($borrowernumber);
2812     my $hasoverdues = C4::Members::HasOverdues($borrowernumber);
2813
2814     if ( $restricted and $restrictionblockrenewing ) {
2815         return ( 0, 'restriction');
2816     } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($itemissue->{overdue} and $overduesblockrenewing eq 'blockitem') ) {
2817         return ( 0, 'overdue');
2818     }
2819
2820     if ( $issuingrule->{norenewalbefore} ) {
2821
2822         # Get current time and add norenewalbefore.
2823         # If this is smaller than date_due, it's too soon for renewal.
2824         if (
2825             DateTime->now( time_zone => C4::Context->tz() )->add(
2826                 $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore}
2827             ) < $itemissue->{date_due}
2828           )
2829         {
2830             return ( 0, "auto_too_soon" ) if $itemissue->{auto_renew};
2831             return ( 0, "too_soon" );
2832         }
2833     }
2834
2835     return ( 0, "auto_renew" ) if $itemissue->{auto_renew};
2836     return ( 1, undef );
2837 }
2838
2839 =head2 AddRenewal
2840
2841   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2842
2843 Renews a loan.
2844
2845 C<$borrowernumber> is the borrower number of the patron who currently
2846 has the item.
2847
2848 C<$itemnumber> is the number of the item to renew.
2849
2850 C<$branch> is the library where the renewal took place (if any).
2851            The library that controls the circ policies for the renewal is retrieved from the issues record.
2852
2853 C<$datedue> can be a DateTime object used to set the due date.
2854
2855 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2856 this parameter is not supplied, lastreneweddate is set to the current date.
2857
2858 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2859 from the book's item type.
2860
2861 =cut
2862
2863 sub AddRenewal {
2864     my $borrowernumber  = shift;
2865     my $itemnumber      = shift or return;
2866     my $branch          = shift;
2867     my $datedue         = shift;
2868     my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2869
2870     my $item   = GetItem($itemnumber) or return;
2871     my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2872
2873     my $dbh = C4::Context->dbh;
2874
2875     # Find the issues record for this book
2876     my $sth =
2877       $dbh->prepare("SELECT * FROM issues WHERE itemnumber = ?");
2878     $sth->execute( $itemnumber );
2879     my $issuedata = $sth->fetchrow_hashref;
2880
2881     return unless ( $issuedata );
2882
2883     $borrowernumber ||= $issuedata->{borrowernumber};
2884
2885     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2886         carp 'Invalid date passed to AddRenewal.';
2887         return;
2888     }
2889
2890     # If the due date wasn't specified, calculate it by adding the
2891     # book's loan length to today's date or the current due date
2892     # based on the value of the RenewalPeriodBase syspref.
2893     unless ($datedue) {
2894
2895         my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2896         my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2897
2898         $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2899                                         dt_from_string( $issuedata->{date_due} ) :
2900                                         DateTime->now( time_zone => C4::Context->tz());
2901         $datedue =  CalcDateDue($datedue, $itemtype, $issuedata->{'branchcode'}, $borrower, 'is a renewal');
2902     }
2903
2904     # Update the issues record to have the new due date, and a new count
2905     # of how many times it has been renewed.
2906     my $renews = $issuedata->{'renewals'} + 1;
2907     $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2908                             WHERE borrowernumber=? 
2909                             AND itemnumber=?"
2910     );
2911
2912     $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2913
2914     # Update the renewal count on the item, and tell zebra to reindex
2915     $renews = $biblio->{'renewals'} + 1;
2916     ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2917
2918     # Charge a new rental fee, if applicable?
2919     my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2920     if ( $charge > 0 ) {
2921         my $accountno = getnextacctno( $borrowernumber );
2922         my $item = GetBiblioFromItemNumber($itemnumber);
2923         my $manager_id = 0;
2924         $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; 
2925         $sth = $dbh->prepare(
2926                 "INSERT INTO accountlines
2927                     (date, borrowernumber, accountno, amount, manager_id,
2928                     description,accounttype, amountoutstanding, itemnumber)
2929                     VALUES (now(),?,?,?,?,?,?,?,?)"
2930         );
2931         $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2932             "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2933             'Rent', $charge, $itemnumber );
2934     }
2935
2936     # Send a renewal slip according to checkout alert preferencei
2937     if ( C4::Context->preference('RenewalSendNotice') eq '1') {
2938         my $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 );
2939         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2940         my %conditions = (
2941                 branchcode   => $branch,
2942                 categorycode => $borrower->{categorycode},
2943                 item_type    => $item->{itype},
2944                 notification => 'CHECKOUT',
2945         );
2946         if ($circulation_alert->is_enabled_for(\%conditions)) {
2947                 SendCirculationAlert({
2948                         type     => 'RENEWAL',
2949                         item     => $item,
2950                 borrower => $borrower,
2951                 branch   => $branch,
2952                 });
2953         }
2954     }
2955
2956     # Remove any OVERDUES related debarment if the borrower has no overdues
2957     my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
2958     if ( $borrowernumber
2959       && $borrower->{'debarred'}
2960       && !C4::Members::HasOverdues( $borrowernumber )
2961       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2962     ) {
2963         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2964     }
2965
2966     # Log the renewal
2967     UpdateStats({branch => $branch,
2968                 type => 'renew',
2969                 amount => $charge,
2970                 itemnumber => $itemnumber,
2971                 itemtype => $item->{itype},
2972                 borrowernumber => $borrowernumber,
2973                 ccode => $item->{'ccode'}}
2974                 );
2975         return $datedue;
2976 }
2977
2978 sub GetRenewCount {
2979     # check renewal status
2980     my ( $bornum, $itemno ) = @_;
2981     my $dbh           = C4::Context->dbh;
2982     my $renewcount    = 0;
2983     my $renewsallowed = 0;
2984     my $renewsleft    = 0;
2985
2986     my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2987     my $item     = GetItem($itemno); 
2988
2989     # Look in the issues table for this item, lent to this borrower,
2990     # and not yet returned.
2991
2992     # FIXME - I think this function could be redone to use only one SQL call.
2993     my $sth = $dbh->prepare(
2994         "select * from issues
2995                                 where (borrowernumber = ?)
2996                                 and (itemnumber = ?)"
2997     );
2998     $sth->execute( $bornum, $itemno );
2999     my $data = $sth->fetchrow_hashref;
3000     $renewcount = $data->{'renewals'} if $data->{'renewals'};
3001     # $item and $borrower should be calculated
3002     my $branchcode = _GetCircControlBranch($item, $borrower);
3003     
3004     my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
3005     
3006     $renewsallowed = $issuingrule->{'renewalsallowed'};
3007     $renewsleft    = $renewsallowed - $renewcount;
3008     if($renewsleft < 0){ $renewsleft = 0; }
3009     return ( $renewcount, $renewsallowed, $renewsleft );
3010 }
3011
3012 =head2 GetSoonestRenewDate
3013
3014   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3015
3016 Find out the soonest possible renew date of a borrowed item.
3017
3018 C<$borrowernumber> is the borrower number of the patron who currently
3019 has the item on loan.
3020
3021 C<$itemnumber> is the number of the item to renew.
3022
3023 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3024 renew date, based on the value "No renewal before" of the applicable
3025 issuing rule. Returns the current date if the item can already be
3026 renewed, and returns undefined if the borrower, loan, or item
3027 cannot be found.
3028
3029 =cut
3030
3031 sub GetSoonestRenewDate {
3032     my ( $borrowernumber, $itemnumber ) = @_;
3033
3034     my $dbh = C4::Context->dbh;
3035
3036     my $item      = GetItem($itemnumber)      or return;
3037     my $itemissue = GetItemIssue($itemnumber) or return;
3038
3039     $borrowernumber ||= $itemissue->{borrowernumber};
3040     my $borrower = C4::Members::GetMemberDetails($borrowernumber)
3041       or return;
3042
3043     my $branchcode = _GetCircControlBranch( $item, $borrower );
3044     my $issuingrule =
3045       GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
3046
3047     my $now = DateTime->now( time_zone => C4::Context->tz() );
3048
3049     if ( $issuingrule->{norenewalbefore} ) {
3050         my $soonestrenewal =
3051           $itemissue->{date_due}->subtract(
3052             $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} );
3053
3054         $soonestrenewal = $now > $soonestrenewal ? $now : $soonestrenewal;
3055         return $soonestrenewal;
3056     }
3057     return $now;
3058 }
3059
3060 =head2 GetIssuingCharges
3061
3062   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3063
3064 Calculate how much it would cost for a given patron to borrow a given
3065 item, including any applicable discounts.
3066
3067 C<$itemnumber> is the item number of item the patron wishes to borrow.
3068
3069 C<$borrowernumber> is the patron's borrower number.
3070
3071 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3072 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3073 if it's a video).
3074
3075 =cut
3076
3077 sub GetIssuingCharges {
3078
3079     # calculate charges due
3080     my ( $itemnumber, $borrowernumber ) = @_;
3081     my $charge = 0;
3082     my $dbh    = C4::Context->dbh;
3083     my $item_type;
3084
3085     # Get the book's item type and rental charge (via its biblioitem).
3086     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3087         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3088     $charge_query .= (C4::Context->preference('item-level_itypes'))
3089         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3090         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3091
3092     $charge_query .= ' WHERE items.itemnumber =?';
3093
3094     my $sth = $dbh->prepare($charge_query);
3095     $sth->execute($itemnumber);
3096     if ( my $item_data = $sth->fetchrow_hashref ) {
3097         $item_type = $item_data->{itemtype};
3098         $charge    = $item_data->{rentalcharge};
3099         my $branch = C4::Branch::mybranch();
3100         my $discount_query = q|SELECT rentaldiscount,
3101             issuingrules.itemtype, issuingrules.branchcode
3102             FROM borrowers
3103             LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3104             WHERE borrowers.borrowernumber = ?
3105             AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3106             AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3107         my $discount_sth = $dbh->prepare($discount_query);
3108         $discount_sth->execute( $borrowernumber, $item_type, $branch );
3109         my $discount_rules = $discount_sth->fetchall_arrayref({});
3110         if (@{$discount_rules}) {
3111             # We may have multiple rules so get the most specific
3112             my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3113             $charge = ( $charge * ( 100 - $discount ) ) / 100;
3114         }
3115     }
3116
3117     return ( $charge, $item_type );
3118 }
3119
3120 # Select most appropriate discount rule from those returned
3121 sub _get_discount_from_rule {
3122     my ($rules_ref, $branch, $itemtype) = @_;
3123     my $discount;
3124
3125     if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3126         $discount = $rules_ref->[0]->{rentaldiscount};
3127         return (defined $discount) ? $discount : 0;
3128     }
3129     # could have up to 4 does one match $branch and $itemtype
3130     my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3131     if (@d) {
3132         $discount = $d[0]->{rentaldiscount};
3133         return (defined $discount) ? $discount : 0;
3134     }
3135     # do we have item type + all branches
3136     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3137     if (@d) {
3138         $discount = $d[0]->{rentaldiscount};
3139         return (defined $discount) ? $discount : 0;
3140     }
3141     # do we all item types + this branch
3142     @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3143     if (@d) {
3144         $discount = $d[0]->{rentaldiscount};
3145         return (defined $discount) ? $discount : 0;
3146     }
3147     # so all and all (surely we wont get here)
3148     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3149     if (@d) {
3150         $discount = $d[0]->{rentaldiscount};
3151         return (defined $discount) ? $discount : 0;
3152     }
3153     # none of the above
3154     return 0;
3155 }
3156
3157 =head2 AddIssuingCharge
3158
3159   &AddIssuingCharge( $itemno, $borrowernumber, $charge )
3160
3161 =cut
3162
3163 sub AddIssuingCharge {
3164     my ( $itemnumber, $borrowernumber, $charge ) = @_;
3165     my $dbh = C4::Context->dbh;
3166     my $nextaccntno = getnextacctno( $borrowernumber );
3167     my $manager_id = 0;
3168     $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
3169     my $query ="
3170         INSERT INTO accountlines
3171             (borrowernumber, itemnumber, accountno,
3172             date, amount, description, accounttype,
3173             amountoutstanding, manager_id)
3174         VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
3175     ";
3176     my $sth = $dbh->prepare($query);
3177     $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
3178 }
3179
3180 =head2 GetTransfers
3181
3182   GetTransfers($itemnumber);
3183
3184 =cut
3185
3186 sub GetTransfers {
3187     my ($itemnumber) = @_;
3188
3189     my $dbh = C4::Context->dbh;
3190
3191     my $query = '
3192         SELECT datesent,
3193                frombranch,
3194                tobranch
3195         FROM branchtransfers
3196         WHERE itemnumber = ?
3197           AND datearrived IS NULL
3198         ';
3199     my $sth = $dbh->prepare($query);
3200     $sth->execute($itemnumber);
3201     my @row = $sth->fetchrow_array();
3202     return @row;
3203 }
3204
3205 =head2 GetTransfersFromTo
3206
3207   @results = GetTransfersFromTo($frombranch,$tobranch);
3208
3209 Returns the list of pending transfers between $from and $to branch
3210
3211 =cut
3212
3213 sub GetTransfersFromTo {
3214     my ( $frombranch, $tobranch ) = @_;
3215     return unless ( $frombranch && $tobranch );
3216     my $dbh   = C4::Context->dbh;
3217     my $query = "
3218         SELECT itemnumber,datesent,frombranch
3219         FROM   branchtransfers
3220         WHERE  frombranch=?
3221           AND  tobranch=?
3222           AND datearrived IS NULL
3223     ";
3224     my $sth = $dbh->prepare($query);
3225     $sth->execute( $frombranch, $tobranch );
3226     my @gettransfers;
3227
3228     while ( my $data = $sth->fetchrow_hashref ) {
3229         push @gettransfers, $data;
3230     }
3231     return (@gettransfers);
3232 }
3233
3234 =head2 DeleteTransfer
3235
3236   &DeleteTransfer($itemnumber);
3237
3238 =cut
3239
3240 sub DeleteTransfer {
3241     my ($itemnumber) = @_;
3242     return unless $itemnumber;
3243     my $dbh          = C4::Context->dbh;
3244     my $sth          = $dbh->prepare(
3245         "DELETE FROM branchtransfers
3246          WHERE itemnumber=?
3247          AND datearrived IS NULL "
3248     );
3249     return $sth->execute($itemnumber);
3250 }
3251
3252 =head2 AnonymiseIssueHistory
3253
3254   ($rows,$err_history_not_deleted) = AnonymiseIssueHistory($date,$borrowernumber)
3255
3256 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
3257 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
3258
3259 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
3260 setting (force delete).
3261
3262 return the number of affected rows and a value that evaluates to true if an error occurred deleting the history.
3263
3264 =cut
3265
3266 sub AnonymiseIssueHistory {
3267     my $date           = shift;
3268     my $borrowernumber = shift;
3269     my $dbh            = C4::Context->dbh;
3270     my $query          = "
3271         UPDATE old_issues
3272         SET    borrowernumber = ?
3273         WHERE  returndate < ?
3274           AND borrowernumber IS NOT NULL
3275     ";
3276
3277     # The default of 0 does not work due to foreign key constraints
3278     # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
3279     # Set it to undef (NULL)
3280     my $anonymouspatron = C4::Context->preference('AnonymousPatron') || undef;
3281     my @bind_params = ($anonymouspatron, $date);
3282     if (defined $borrowernumber) {
3283        $query .= " AND borrowernumber = ?";
3284        push @bind_params, $borrowernumber;
3285     } else {
3286        $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
3287     }
3288     my $sth = $dbh->prepare($query);
3289     $sth->execute(@bind_params);
3290     my $anonymisation_err = $dbh->err;
3291     my $rows_affected = $sth->rows;  ### doublecheck row count return function
3292     return ($rows_affected, $anonymisation_err);
3293 }
3294
3295 =head2 SendCirculationAlert
3296
3297 Send out a C<check-in> or C<checkout> alert using the messaging system.
3298
3299 B<Parameters>:
3300
3301 =over 4
3302
3303 =item type
3304
3305 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3306
3307 =item item
3308
3309 Hashref of information about the item being checked in or out.
3310
3311 =item borrower
3312
3313 Hashref of information about the borrower of the item.
3314
3315 =item branch
3316
3317 The branchcode from where the checkout or check-in took place.
3318
3319 =back
3320
3321 B<Example>:
3322
3323     SendCirculationAlert({
3324         type     => 'CHECKOUT',
3325         item     => $item,
3326         borrower => $borrower,
3327         branch   => $branch,
3328     });
3329
3330 =cut
3331
3332 sub SendCirculationAlert {
3333     my ($opts) = @_;
3334     my ($type, $item, $borrower, $branch) =
3335         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3336     my %message_name = (
3337         CHECKIN  => 'Item_Check_in',
3338         CHECKOUT => 'Item_Checkout',
3339         RENEWAL  => 'Item_Checkout',
3340     );
3341     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3342         borrowernumber => $borrower->{borrowernumber},
3343         message_name   => $message_name{$type},
3344     });
3345     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3346
3347     my @transports = keys %{ $borrower_preferences->{transports} };
3348     # warn "no transports" unless @transports;
3349     for (@transports) {
3350         # warn "transport: $_";
3351         my $message = C4::Message->find_last_message($borrower, $type, $_);
3352         if (!$message) {
3353             #warn "create new message";
3354             my $letter =  C4::Letters::GetPreparedLetter (
3355                 module => 'circulation',
3356                 letter_code => $type,
3357                 branchcode => $branch,
3358                 message_transport_type => $_,
3359                 tables => {
3360                     $issues_table => $item->{itemnumber},
3361                     'items'       => $item->{itemnumber},
3362                     'biblio'      => $item->{biblionumber},
3363                     'biblioitems' => $item->{biblionumber},
3364                     'borrowers'   => $borrower,
3365                     'branches'    => $branch,
3366                 }
3367             ) or next;
3368             C4::Message->enqueue($letter, $borrower, $_);
3369         } else {
3370             #warn "append to old message";
3371             my $letter =  C4::Letters::GetPreparedLetter (
3372                 module => 'circulation',
3373                 letter_code => $type,
3374                 branchcode => $branch,
3375                 message_transport_type => $_,
3376                 tables => {
3377                     $issues_table => $item->{itemnumber},
3378                     'items'       => $item->{itemnumber},
3379                     'biblio'      => $item->{biblionumber},
3380                     'biblioitems' => $item->{biblionumber},
3381                     'borrowers'   => $borrower,
3382                     'branches'    => $branch,
3383                 }
3384             ) or next;
3385             $message->append($letter);
3386             $message->update;
3387         }
3388     }
3389
3390     return;
3391 }
3392
3393 =head2 updateWrongTransfer
3394
3395   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3396
3397 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 
3398
3399 =cut
3400
3401 sub updateWrongTransfer {
3402         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3403         my $dbh = C4::Context->dbh;     
3404 # first step validate the actual line of transfert .
3405         my $sth =
3406                 $dbh->prepare(
3407                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3408                 );
3409                 $sth->execute($FromLibrary,$itemNumber);
3410
3411 # second step create a new line of branchtransfer to the right location .
3412         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3413
3414 #third step changing holdingbranch of item
3415         UpdateHoldingbranch($FromLibrary,$itemNumber);
3416 }
3417
3418 =head2 UpdateHoldingbranch
3419
3420   $items = UpdateHoldingbranch($branch,$itmenumber);
3421
3422 Simple methode for updating hodlingbranch in items BDD line
3423
3424 =cut
3425
3426 sub UpdateHoldingbranch {
3427         my ( $branch,$itemnumber ) = @_;
3428     ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3429 }
3430
3431 =head2 CalcDateDue
3432
3433 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3434
3435 this function calculates the due date given the start date and configured circulation rules,
3436 checking against the holidays calendar as per the 'useDaysMode' syspref.
3437 C<$startdate>   = DateTime object representing start date of loan period (assumed to be today)
3438 C<$itemtype>  = itemtype code of item in question
3439 C<$branch>  = location whose calendar to use
3440 C<$borrower> = Borrower object
3441 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3442
3443 =cut
3444
3445 sub CalcDateDue {
3446     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3447
3448     $isrenewal ||= 0;
3449
3450     # loanlength now a href
3451     my $loanlength =
3452             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3453
3454     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3455             ? qq{renewalperiod}
3456             : qq{issuelength};
3457
3458     my $datedue;
3459     if ( $startdate ) {
3460         if (ref $startdate ne 'DateTime' ) {
3461             $datedue = dt_from_string($datedue);
3462         } else {
3463             $datedue = $startdate->clone;
3464         }
3465     } else {
3466         $datedue =
3467           DateTime->now( time_zone => C4::Context->tz() )
3468           ->truncate( to => 'minute' );
3469     }
3470
3471
3472     # calculate the datedue as normal
3473     if ( C4::Context->preference('useDaysMode') eq 'Days' )
3474     {    # ignoring calendar
3475         if ( $loanlength->{lengthunit} eq 'hours' ) {
3476             $datedue->add( hours => $loanlength->{$length_key} );
3477         } else {    # days
3478             $datedue->add( days => $loanlength->{$length_key} );
3479             $datedue->set_hour(23);
3480             $datedue->set_minute(59);
3481         }
3482     } else {
3483         my $dur;
3484         if ($loanlength->{lengthunit} eq 'hours') {
3485             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3486         }
3487         else { # days
3488             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3489         }
3490         my $calendar = Koha::Calendar->new( branchcode => $branch );
3491         $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3492         if ($loanlength->{lengthunit} eq 'days') {
3493             $datedue->set_hour(23);
3494             $datedue->set_minute(59);
3495         }
3496     }
3497
3498     # if Hard Due Dates are used, retrieve them and apply as necessary
3499     my ( $hardduedate, $hardduedatecompare ) =
3500       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3501     if ($hardduedate) {    # hardduedates are currently dates
3502         $hardduedate->truncate( to => 'minute' );
3503         $hardduedate->set_hour(23);
3504         $hardduedate->set_minute(59);
3505         my $cmp = DateTime->compare( $hardduedate, $datedue );
3506
3507 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3508 # if the calculated date is before the 'after' Hard Due Date (floor), override
3509 # if the hard due date is set to 'exactly', overrride
3510         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3511             $datedue = $hardduedate->clone;
3512         }
3513
3514         # in all other cases, keep the date due as it is
3515
3516     }
3517
3518     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3519     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3520         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3521         if( $expiry_dt ) { #skip empty expiry date..
3522             $expiry_dt->set( hour => 23, minute => 59);
3523             my $d1= $datedue->clone->set_time_zone('floating');
3524             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3525                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3526             }
3527         }
3528     }
3529
3530     return $datedue;
3531 }
3532
3533
3534 =head2 CheckRepeatableHolidays
3535
3536   $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
3537
3538 This function checks if the date due is a repeatable holiday
3539
3540 C<$date_due>   = returndate calculate with no day check
3541 C<$itemnumber>  = itemnumber
3542 C<$branchcode>  = localisation of issue 
3543
3544 =cut
3545
3546 sub CheckRepeatableHolidays{
3547 my($itemnumber,$week_day,$branchcode)=@_;
3548 my $dbh = C4::Context->dbh;
3549 my $query = qq|SELECT count(*)  
3550         FROM repeatable_holidays 
3551         WHERE branchcode=?
3552         AND weekday=?|;
3553 my $sth = $dbh->prepare($query);
3554 $sth->execute($branchcode,$week_day);
3555 my $result=$sth->fetchrow;
3556 return $result;
3557 }
3558
3559
3560 =head2 CheckSpecialHolidays
3561
3562   $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
3563
3564 This function check if the date is a special holiday
3565
3566 C<$years>   = the years of datedue
3567 C<$month>   = the month of datedue
3568 C<$day>     = the day of datedue
3569 C<$itemnumber>  = itemnumber
3570 C<$branchcode>  = localisation of issue 
3571
3572 =cut
3573
3574 sub CheckSpecialHolidays{
3575 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
3576 my $dbh = C4::Context->dbh;
3577 my $query=qq|SELECT count(*) 
3578              FROM `special_holidays`
3579              WHERE year=?
3580              AND month=?
3581              AND day=?
3582              AND branchcode=?
3583             |;
3584 my $sth = $dbh->prepare($query);
3585 $sth->execute($years,$month,$day,$branchcode);
3586 my $countspecial=$sth->fetchrow ;
3587 return $countspecial;
3588 }
3589
3590 =head2 CheckRepeatableSpecialHolidays
3591
3592   $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
3593
3594 This function check if the date is a repeatble special holidays
3595
3596 C<$month>   = the month of datedue
3597 C<$day>     = the day of datedue
3598 C<$itemnumber>  = itemnumber
3599 C<$branchcode>  = localisation of issue 
3600
3601 =cut
3602
3603 sub CheckRepeatableSpecialHolidays{
3604 my ($month,$day,$itemnumber,$branchcode) = @_;
3605 my $dbh = C4::Context->dbh;
3606 my $query=qq|SELECT count(*) 
3607              FROM `repeatable_holidays`
3608              WHERE month=?
3609              AND day=?
3610              AND branchcode=?
3611             |;
3612 my $sth = $dbh->prepare($query);
3613 $sth->execute($month,$day,$branchcode);
3614 my $countspecial=$sth->fetchrow ;
3615 return $countspecial;
3616 }
3617
3618
3619
3620 sub CheckValidBarcode{
3621 my ($barcode) = @_;
3622 my $dbh = C4::Context->dbh;
3623 my $query=qq|SELECT count(*) 
3624              FROM items 
3625              WHERE barcode=?
3626             |;
3627 my $sth = $dbh->prepare($query);
3628 $sth->execute($barcode);
3629 my $exist=$sth->fetchrow ;
3630 return $exist;
3631 }
3632
3633 =head2 IsBranchTransferAllowed
3634
3635   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3636
3637 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3638
3639 =cut
3640
3641 sub IsBranchTransferAllowed {
3642         my ( $toBranch, $fromBranch, $code ) = @_;
3643
3644         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3645         
3646         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3647         my $dbh = C4::Context->dbh;
3648             
3649         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3650         $sth->execute( $toBranch, $fromBranch, $code );
3651         my $limit = $sth->fetchrow_hashref();
3652                         
3653         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3654         if ( $limit->{'limitId'} ) {
3655                 return 0;
3656         } else {
3657                 return 1;
3658         }
3659 }                                                        
3660
3661 =head2 CreateBranchTransferLimit
3662
3663   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3664
3665 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3666
3667 =cut
3668
3669 sub CreateBranchTransferLimit {
3670    my ( $toBranch, $fromBranch, $code ) = @_;
3671    return unless defined($toBranch) && defined($fromBranch);
3672    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3673    
3674    my $dbh = C4::Context->dbh;
3675    
3676    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3677    return $sth->execute( $code, $toBranch, $fromBranch );
3678 }
3679
3680 =head2 DeleteBranchTransferLimits
3681
3682     my $result = DeleteBranchTransferLimits($frombranch);
3683
3684 Deletes all the library transfer limits for one library.  Returns the
3685 number of limits deleted, 0e0 if no limits were deleted, or undef if
3686 no arguments are supplied.
3687
3688 =cut
3689
3690 sub DeleteBranchTransferLimits {
3691     my $branch = shift;
3692     return unless defined $branch;
3693     my $dbh    = C4::Context->dbh;
3694     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3695     return $sth->execute($branch);
3696 }
3697
3698 sub ReturnLostItem{
3699     my ( $borrowernumber, $itemnum ) = @_;
3700
3701     MarkIssueReturned( $borrowernumber, $itemnum );
3702     my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3703     my $item = C4::Items::GetItem( $itemnum );
3704     my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3705     my @datearr = localtime(time);
3706     my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3707     my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3708     ModItem({ paidfor =>  $old_note."Paid for by $bor $date" }, undef, $itemnum);
3709 }
3710
3711
3712 sub LostItem{
3713     my ($itemnumber, $mark_returned) = @_;
3714
3715     my $dbh = C4::Context->dbh();
3716     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3717                            FROM issues 
3718                            JOIN items USING (itemnumber) 
3719                            JOIN biblio USING (biblionumber)
3720                            WHERE issues.itemnumber=?");
3721     $sth->execute($itemnumber);
3722     my $issues=$sth->fetchrow_hashref();
3723
3724     # If a borrower lost the item, add a replacement cost to the their record
3725     if ( my $borrowernumber = $issues->{borrowernumber} ){
3726         my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3727
3728         if (C4::Context->preference('WhenLostForgiveFine')){
3729             my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3730             defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3731         }
3732         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3733             C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3734             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3735             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3736         }
3737
3738         MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3739     }
3740 }
3741
3742 sub GetOfflineOperations {
3743     my $dbh = C4::Context->dbh;
3744     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3745     $sth->execute(C4::Context->userenv->{'branch'});
3746     my $results = $sth->fetchall_arrayref({});
3747     return $results;
3748 }
3749
3750 sub GetOfflineOperation {
3751     my $operationid = shift;
3752     return unless $operationid;
3753     my $dbh = C4::Context->dbh;
3754     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3755     $sth->execute( $operationid );
3756     return $sth->fetchrow_hashref;
3757 }
3758
3759 sub AddOfflineOperation {
3760     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3761     my $dbh = C4::Context->dbh;
3762     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3763     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3764     return "Added.";
3765 }
3766
3767 sub DeleteOfflineOperation {
3768     my $dbh = C4::Context->dbh;
3769     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3770     $sth->execute( shift );
3771     return "Deleted.";
3772 }
3773
3774 sub ProcessOfflineOperation {
3775     my $operation = shift;
3776
3777     my $report;
3778     if ( $operation->{action} eq 'return' ) {
3779         $report = ProcessOfflineReturn( $operation );
3780     } elsif ( $operation->{action} eq 'issue' ) {
3781         $report = ProcessOfflineIssue( $operation );
3782     } elsif ( $operation->{action} eq 'payment' ) {
3783         $report = ProcessOfflinePayment( $operation );
3784     }
3785
3786     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3787
3788     return $report;
3789 }
3790
3791 sub ProcessOfflineReturn {
3792     my $operation = shift;
3793
3794     my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3795
3796     if ( $itemnumber ) {
3797         my $issue = GetOpenIssue( $itemnumber );
3798         if ( $issue ) {
3799             MarkIssueReturned(
3800                 $issue->{borrowernumber},
3801                 $itemnumber,
3802                 undef,
3803                 $operation->{timestamp},
3804             );
3805             ModItem(
3806                 { renewals => 0, onloan => undef },
3807                 $issue->{'biblionumber'},
3808                 $itemnumber
3809             );
3810             return "Success.";
3811         } else {
3812             return "Item not issued.";
3813         }
3814     } else {
3815         return "Item not found.";
3816     }
3817 }
3818
3819 sub ProcessOfflineIssue {
3820     my $operation = shift;
3821
3822     my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3823
3824     if ( $borrower->{borrowernumber} ) {
3825         my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3826         unless ($itemnumber) {
3827             return "Barcode not found.";
3828         }
3829         my $issue = GetOpenIssue( $itemnumber );
3830
3831         if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3832             MarkIssueReturned(
3833                 $issue->{borrowernumber},
3834                 $itemnumber,
3835                 undef,
3836                 $operation->{timestamp},
3837             );
3838         }
3839         AddIssue(
3840             $borrower,
3841             $operation->{'barcode'},
3842             undef,
3843             1,
3844             $operation->{timestamp},
3845             undef,
3846         );
3847         return "Success.";
3848     } else {
3849         return "Borrower not found.";
3850     }
3851 }
3852
3853 sub ProcessOfflinePayment {
3854     my $operation = shift;
3855
3856     my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3857     my $amount = $operation->{amount};
3858
3859     recordpayment( $borrower->{borrowernumber}, $amount );
3860
3861     return "Success."
3862 }
3863
3864
3865 =head2 TransferSlip
3866
3867   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3868
3869   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3870
3871 =cut
3872
3873 sub TransferSlip {
3874     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3875
3876     my $item =  GetItem( $itemnumber, $barcode )
3877       or return;
3878
3879     return C4::Letters::GetPreparedLetter (
3880         module => 'circulation',
3881         letter_code => 'TRANSFERSLIP',
3882         branchcode => $branch,
3883         tables => {
3884             'branches'    => $to_branch,
3885             'biblio'      => $item->{biblionumber},
3886             'items'       => $item,
3887         },
3888     );
3889 }
3890
3891 =head2 CheckIfIssuedToPatron
3892
3893   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3894
3895   Return 1 if any record item is issued to patron, otherwise return 0
3896
3897 =cut
3898
3899 sub CheckIfIssuedToPatron {
3900     my ($borrowernumber, $biblionumber) = @_;
3901
3902     my $dbh = C4::Context->dbh;
3903     my $query = q|
3904         SELECT COUNT(*) FROM issues
3905         LEFT JOIN items ON items.itemnumber = issues.itemnumber
3906         WHERE items.biblionumber = ?
3907         AND issues.borrowernumber = ?
3908     |;
3909     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3910     return 1 if $is_issued;
3911     return;
3912 }
3913
3914 =head2 IsItemIssued
3915
3916   IsItemIssued( $itemnumber )
3917
3918   Return 1 if the item is on loan, otherwise return 0
3919
3920 =cut
3921
3922 sub IsItemIssued {
3923     my $itemnumber = shift;
3924     my $dbh = C4::Context->dbh;
3925     my $sth = $dbh->prepare(q{
3926         SELECT COUNT(*)
3927         FROM issues
3928         WHERE itemnumber = ?
3929     });
3930     $sth->execute($itemnumber);
3931     return $sth->fetchrow;
3932 }
3933
3934 =head2 GetAgeRestriction
3935
3936   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3937   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3938
3939   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as he is older or as old as the agerestriction }
3940   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3941
3942 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3943 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3944 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3945          Negative days mean the borrower has gone past the age restriction age.
3946
3947 =cut
3948
3949 sub GetAgeRestriction {
3950     my ($record_restrictions, $borrower) = @_;
3951     my $markers = C4::Context->preference('AgeRestrictionMarker');
3952
3953     # Split $record_restrictions to something like FSK 16 or PEGI 6
3954     my @values = split ' ', uc($record_restrictions);
3955     return unless @values;
3956
3957     # Search first occurrence of one of the markers
3958     my @markers = split /\|/, uc($markers);
3959     return unless @markers;
3960
3961     my $index            = 0;
3962     my $restriction_year = 0;
3963     for my $value (@values) {
3964         $index++;
3965         for my $marker (@markers) {
3966             $marker =~ s/^\s+//;    #remove leading spaces
3967             $marker =~ s/\s+$//;    #remove trailing spaces
3968             if ( $marker eq $value ) {
3969                 if ( $index <= $#values ) {
3970                     $restriction_year += $values[$index];
3971                 }
3972                 last;
3973             }
3974             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3975
3976                 # Perhaps it is something like "K16" (as in Finland)
3977                 $restriction_year += $1;
3978                 last;
3979             }
3980         }
3981         last if ( $restriction_year > 0 );
3982     }
3983
3984     #Check if the borrower is age restricted for this material and for how long.
3985     if ($restriction_year && $borrower) {
3986         if ( $borrower->{'dateofbirth'} ) {
3987             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3988             $alloweddate[0] += $restriction_year;
3989
3990             #Prevent runime eror on leap year (invalid date)
3991             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3992                 $alloweddate[2] = 28;
3993             }
3994
3995             #Get how many days the borrower has to reach the age restriction
3996             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(Today);
3997             #Negative days means the borrower went past the age restriction age
3998             return ($restriction_year, $daysToAgeRestriction);
3999         }
4000     }
4001
4002     return ($restriction_year);
4003 }
4004
4005 1;
4006
4007 =head2 GetPendingOnSiteCheckouts
4008
4009 =cut
4010
4011 sub GetPendingOnSiteCheckouts {
4012     my $dbh = C4::Context->dbh;
4013     return $dbh->selectall_arrayref(q|
4014         SELECT
4015           items.barcode,
4016           items.biblionumber,
4017           items.itemnumber,
4018           items.itemnotes,
4019           items.itemcallnumber,
4020           items.location,
4021           issues.date_due,
4022           issues.branchcode,
4023           issues.date_due < NOW() AS is_overdue,
4024           biblio.author,
4025           biblio.title,
4026           borrowers.firstname,
4027           borrowers.surname,
4028           borrowers.cardnumber,
4029           borrowers.borrowernumber
4030         FROM items
4031         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4032         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4033         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4034         WHERE issues.onsite_checkout = 1
4035     |, { Slice => {} } );
4036 }
4037
4038 sub GetTopIssues {
4039     my ($params) = @_;
4040
4041     my ($count, $branch, $itemtype, $ccode, $newness)
4042         = @$params{qw(count branch itemtype ccode newness)};
4043
4044     my $dbh = C4::Context->dbh;
4045     my $query = q{
4046         SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4047           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4048           i.ccode, SUM(i.issues) AS count
4049         FROM biblio b
4050         LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4051         LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4052     };
4053
4054     my (@where_strs, @where_args);
4055
4056     if ($branch) {
4057         push @where_strs, 'i.homebranch = ?';
4058         push @where_args, $branch;
4059     }
4060     if ($itemtype) {
4061         if (C4::Context->preference('item-level_itypes')){
4062             push @where_strs, 'i.itype = ?';
4063             push @where_args, $itemtype;
4064         } else {
4065             push @where_strs, 'bi.itemtype = ?';
4066             push @where_args, $itemtype;
4067         }
4068     }
4069     if ($ccode) {
4070         push @where_strs, 'i.ccode = ?';
4071         push @where_args, $ccode;
4072     }
4073     if ($newness) {
4074         push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4075         push @where_args, $newness;
4076     }
4077
4078     if (@where_strs) {
4079         $query .= 'WHERE ' . join(' AND ', @where_strs);
4080     }
4081
4082     $query .= q{
4083         GROUP BY b.biblionumber
4084         HAVING count > 0
4085         ORDER BY count DESC
4086     };
4087
4088     $count = int($count);
4089     if ($count > 0) {
4090         $query .= "LIMIT $count";
4091     }
4092
4093     my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4094
4095     return @$rows;
4096 }
4097
4098 __END__
4099
4100 =head1 AUTHOR
4101
4102 Koha Development Team <http://koha-community.org/>
4103
4104 =cut
4105