Enhancement 7144: Floating Collections (per branch/itemtype)
[koha.git] / C4 / Circulation.pm
blobb6e86e0d45aedab5ed188a6792c2c4f614a43d06
1 package C4::Circulation;
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
11 # version.
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along
18 # with Koha; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use C4::Context;
25 use C4::Stats;
26 use C4::Reserves;
27 use C4::Biblio;
28 use C4::Items;
29 use C4::Members;
30 use C4::Dates;
31 use C4::Calendar;
32 use C4::Accounts;
33 use C4::ItemCirculationAlertPreference;
34 use C4::Dates qw(format_date);
35 use C4::Message;
36 use C4::Debug;
37 use Date::Calc qw(
38 Today
39 Today_and_Now
40 Add_Delta_YM
41 Add_Delta_DHMS
42 Date_to_Days
43 Day_of_Week
44 Add_Delta_Days
45 check_date
46 Delta_Days
48 use POSIX qw(strftime);
49 use C4::Branch; # GetBranches
50 use C4::Log; # logaction
52 use Data::Dumper;
54 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
56 BEGIN {
57 require Exporter;
58 $VERSION = 3.02; # for version checking
59 @ISA = qw(Exporter);
61 # FIXME subs that should probably be elsewhere
62 push @EXPORT, qw(
63 &barcodedecode
64 &LostItem
65 &ReturnLostItem
68 # subs to deal with issuing a book
69 push @EXPORT, qw(
70 &CanBookBeIssued
71 &CanBookBeRenewed
72 &AddIssue
73 &AddRenewal
74 &GetRenewCount
75 &GetItemIssue
76 &GetItemIssues
77 &GetIssuingCharges
78 &GetIssuingRule
79 &GetBranchBorrowerCircRule
80 &GetBranchItemRule
81 &GetBiblioIssues
82 &GetOpenIssue
83 &AnonymiseIssueHistory
86 # subs to deal with returns
87 push @EXPORT, qw(
88 &AddReturn
89 &MarkIssueReturned
92 # subs to deal with transfers
93 push @EXPORT, qw(
94 &transferbook
95 &GetTransfers
96 &GetTransfersFromTo
97 &updateWrongTransfer
98 &DeleteTransfer
99 &IsBranchTransferAllowed
100 &CreateBranchTransferLimit
101 &DeleteBranchTransferLimits
102 &TransferSlip
105 # subs to deal with offline circulation
106 push @EXPORT, qw(
107 &GetOfflineOperations
108 &GetOfflineOperation
109 &AddOfflineOperation
110 &DeleteOfflineOperation
111 &ProcessOfflineOperation
115 =head1 NAME
117 C4::Circulation - Koha circulation module
119 =head1 SYNOPSIS
121 use C4::Circulation;
123 =head1 DESCRIPTION
125 The functions in this module deal with circulation, issues, and
126 returns, as well as general information about the library.
127 Also deals with stocktaking.
129 =head1 FUNCTIONS
131 =head2 barcodedecode
133 $str = &barcodedecode($barcode, [$filter]);
135 Generic filter function for barcode string.
136 Called on every circ if the System Pref itemBarcodeInputFilter is set.
137 Will do some manipulation of the barcode for systems that deliver a barcode
138 to circulation.pl that differs from the barcode stored for the item.
139 For proper functioning of this filter, calling the function on the
140 correct barcode string (items.barcode) should return an unaltered barcode.
142 The optional $filter argument is to allow for testing or explicit
143 behavior that ignores the System Pref. Valid values are the same as the
144 System Pref options.
146 =cut
148 # FIXME -- the &decode fcn below should be wrapped into this one.
149 # FIXME -- these plugins should be moved out of Circulation.pm
151 sub barcodedecode {
152 my ($barcode, $filter) = @_;
153 my $branch = C4::Branch::mybranch();
154 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
155 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
156 if ($filter eq 'whitespace') {
157 $barcode =~ s/\s//g;
158 } elsif ($filter eq 'cuecat') {
159 chomp($barcode);
160 my @fields = split( /\./, $barcode );
161 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
162 ($#results == 2) and return $results[2];
163 } elsif ($filter eq 'T-prefix') {
164 if ($barcode =~ /^[Tt](\d)/) {
165 (defined($1) and $1 eq '0') and return $barcode;
166 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
168 return sprintf("T%07d", $barcode);
169 # FIXME: $barcode could be "T1", causing warning: substr outside of string
170 # Why drop the nonzero digit after the T?
171 # Why pass non-digits (or empty string) to "T%07d"?
172 } elsif ($filter eq 'libsuite8') {
173 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
174 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
175 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
176 }else{
177 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
181 return $barcode; # return barcode, modified or not
184 =head2 decode
186 $str = &decode($chunk);
188 Decodes a segment of a string emitted by a CueCat barcode scanner and
189 returns it.
191 FIXME: Should be replaced with Barcode::Cuecat from CPAN
192 or Javascript based decoding on the client side.
194 =cut
196 sub decode {
197 my ($encoded) = @_;
198 my $seq =
199 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
200 my @s = map { index( $seq, $_ ); } split( //, $encoded );
201 my $l = ( $#s + 1 ) % 4;
202 if ($l) {
203 if ( $l == 1 ) {
204 # warn "Error: Cuecat decode parsing failed!";
205 return;
207 $l = 4 - $l;
208 $#s += $l;
210 my $r = '';
211 while ( $#s >= 0 ) {
212 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
213 $r .=
214 chr( ( $n >> 16 ) ^ 67 )
215 .chr( ( $n >> 8 & 255 ) ^ 67 )
216 .chr( ( $n & 255 ) ^ 67 );
217 @s = @s[ 4 .. $#s ];
219 $r = substr( $r, 0, length($r) - $l );
220 return $r;
223 =head2 transferbook
225 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
226 $barcode, $ignore_reserves);
228 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
230 C<$newbranch> is the code for the branch to which the item should be transferred.
232 C<$barcode> is the barcode of the item to be transferred.
234 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
235 Otherwise, if an item is reserved, the transfer fails.
237 Returns three values:
239 =over
241 =item $dotransfer
243 is true if the transfer was successful.
245 =item $messages
247 is a reference-to-hash which may have any of the following keys:
249 =over
251 =item C<BadBarcode>
253 There is no item in the catalog with the given barcode. The value is C<$barcode>.
255 =item C<IsPermanent>
257 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.
259 =item C<DestinationEqualsHolding>
261 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.
263 =item C<WasReturned>
265 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.
267 =item C<ResFound>
269 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>.
271 =item C<WasTransferred>
273 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
275 =back
277 =back
279 =cut
281 sub transferbook {
282 my ( $tbr, $barcode, $ignoreRs ) = @_;
283 my $messages;
284 my $dotransfer = 1;
285 my $branches = GetBranches();
286 my $itemnumber = GetItemnumberFromBarcode( $barcode );
287 my $issue = GetItemIssue($itemnumber);
288 my $biblio = GetBiblioFromItemNumber($itemnumber);
290 # bad barcode..
291 if ( not $itemnumber ) {
292 $messages->{'BadBarcode'} = $barcode;
293 $dotransfer = 0;
296 # get branches of book...
297 my $hbr = $biblio->{'homebranch'};
298 my $fbr = $biblio->{'holdingbranch'};
300 # if using Branch Transfer Limits
301 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
302 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
303 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
304 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
305 $dotransfer = 0;
307 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
308 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
309 $dotransfer = 0;
313 # if is permanent...
314 if ( $hbr && $branches->{$hbr}->{'PE'} ) {
315 $messages->{'IsPermanent'} = $hbr;
316 $dotransfer = 0;
319 # can't transfer book if is already there....
320 if ( $fbr eq $tbr ) {
321 $messages->{'DestinationEqualsHolding'} = 1;
322 $dotransfer = 0;
325 # check if it is still issued to someone, return it...
326 if ($issue->{borrowernumber}) {
327 AddReturn( $barcode, $fbr );
328 $messages->{'WasReturned'} = $issue->{borrowernumber};
331 # find reserves.....
332 # That'll save a database query.
333 my ( $resfound, $resrec, undef ) =
334 CheckReserves( $itemnumber );
335 if ( $resfound and not $ignoreRs ) {
336 $resrec->{'ResFound'} = $resfound;
338 # $messages->{'ResFound'} = $resrec;
339 $dotransfer = 1;
342 #actually do the transfer....
343 if ($dotransfer) {
344 ModItemTransfer( $itemnumber, $fbr, $tbr );
346 # don't need to update MARC anymore, we do it in batch now
347 $messages->{'WasTransfered'} = 1;
350 ModDateLastSeen( $itemnumber );
351 return ( $dotransfer, $messages, $biblio );
355 sub TooMany {
356 my $borrower = shift;
357 my $biblionumber = shift;
358 my $item = shift;
359 my $cat_borrower = $borrower->{'categorycode'};
360 my $dbh = C4::Context->dbh;
361 my $branch;
362 # Get which branchcode we need
363 $branch = _GetCircControlBranch($item,$borrower);
364 my $type = (C4::Context->preference('item-level_itypes'))
365 ? $item->{'itype'} # item-level
366 : $item->{'itemtype'}; # biblio-level
368 # given branch, patron category, and item type, determine
369 # applicable issuing rule
370 my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
372 # if a rule is found and has a loan limit set, count
373 # how many loans the patron already has that meet that
374 # rule
375 if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
376 my @bind_params;
377 my $count_query = "SELECT COUNT(*) FROM issues
378 JOIN items USING (itemnumber) ";
380 my $rule_itemtype = $issuing_rule->{itemtype};
381 if ($rule_itemtype eq "*") {
382 # matching rule has the default item type, so count only
383 # those existing loans that don't fall under a more
384 # specific rule
385 if (C4::Context->preference('item-level_itypes')) {
386 $count_query .= " WHERE items.itype NOT IN (
387 SELECT itemtype FROM issuingrules
388 WHERE branchcode = ?
389 AND (categorycode = ? OR categorycode = ?)
390 AND itemtype <> '*'
391 ) ";
392 } else {
393 $count_query .= " JOIN biblioitems USING (biblionumber)
394 WHERE biblioitems.itemtype NOT IN (
395 SELECT itemtype FROM issuingrules
396 WHERE branchcode = ?
397 AND (categorycode = ? OR categorycode = ?)
398 AND itemtype <> '*'
399 ) ";
401 push @bind_params, $issuing_rule->{branchcode};
402 push @bind_params, $issuing_rule->{categorycode};
403 push @bind_params, $cat_borrower;
404 } else {
405 # rule has specific item type, so count loans of that
406 # specific item type
407 if (C4::Context->preference('item-level_itypes')) {
408 $count_query .= " WHERE items.itype = ? ";
409 } else {
410 $count_query .= " JOIN biblioitems USING (biblionumber)
411 WHERE biblioitems.itemtype= ? ";
413 push @bind_params, $type;
416 $count_query .= " AND borrowernumber = ? ";
417 push @bind_params, $borrower->{'borrowernumber'};
418 my $rule_branch = $issuing_rule->{branchcode};
419 if ($rule_branch ne "*") {
420 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
421 $count_query .= " AND issues.branchcode = ? ";
422 push @bind_params, $branch;
423 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
424 ; # if branch is the patron's home branch, then count all loans by patron
425 } else {
426 $count_query .= " AND items.homebranch = ? ";
427 push @bind_params, $branch;
431 my $count_sth = $dbh->prepare($count_query);
432 $count_sth->execute(@bind_params);
433 my ($current_loan_count) = $count_sth->fetchrow_array;
435 my $max_loans_allowed = $issuing_rule->{'maxissueqty'};
436 if ($current_loan_count >= $max_loans_allowed) {
437 return ($current_loan_count, $max_loans_allowed);
441 # Now count total loans against the limit for the branch
442 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
443 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
444 my @bind_params = ();
445 my $branch_count_query = "SELECT COUNT(*) FROM issues
446 JOIN items USING (itemnumber)
447 WHERE borrowernumber = ? ";
448 push @bind_params, $borrower->{borrowernumber};
450 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
451 $branch_count_query .= " AND issues.branchcode = ? ";
452 push @bind_params, $branch;
453 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
454 ; # if branch is the patron's home branch, then count all loans by patron
455 } else {
456 $branch_count_query .= " AND items.homebranch = ? ";
457 push @bind_params, $branch;
459 my $branch_count_sth = $dbh->prepare($branch_count_query);
460 $branch_count_sth->execute(@bind_params);
461 my ($current_loan_count) = $branch_count_sth->fetchrow_array;
463 my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty};
464 if ($current_loan_count >= $max_loans_allowed) {
465 return ($current_loan_count, $max_loans_allowed);
469 # OK, the patron can issue !!!
470 return;
473 =head2 itemissues
475 @issues = &itemissues($biblioitemnumber, $biblio);
477 Looks up information about who has borrowed the bookZ<>(s) with the
478 given biblioitemnumber.
480 C<$biblio> is ignored.
482 C<&itemissues> returns an array of references-to-hash. The keys
483 include the fields from the C<items> table in the Koha database.
484 Additional keys include:
486 =over 4
488 =item C<date_due>
490 If the item is currently on loan, this gives the due date.
492 If the item is not on loan, then this is either "Available" or
493 "Cancelled", if the item has been withdrawn.
495 =item C<card>
497 If the item is currently on loan, this gives the card number of the
498 patron who currently has the item.
500 =item C<timestamp0>, C<timestamp1>, C<timestamp2>
502 These give the timestamp for the last three times the item was
503 borrowed.
505 =item C<card0>, C<card1>, C<card2>
507 The card number of the last three patrons who borrowed this item.
509 =item C<borrower0>, C<borrower1>, C<borrower2>
511 The borrower number of the last three patrons who borrowed this item.
513 =back
515 =cut
518 sub itemissues {
519 my ( $bibitem, $biblio ) = @_;
520 my $dbh = C4::Context->dbh;
521 my $sth =
522 $dbh->prepare("Select * from items where items.biblioitemnumber = ?")
523 || die $dbh->errstr;
524 my $i = 0;
525 my @results;
527 $sth->execute($bibitem) || die $sth->errstr;
529 while ( my $data = $sth->fetchrow_hashref ) {
531 # Find out who currently has this item.
532 # FIXME - Wouldn't it be better to do this as a left join of
533 # some sort? Currently, this code assumes that if
534 # fetchrow_hashref() fails, then the book is on the shelf.
535 # fetchrow_hashref() can fail for any number of reasons (e.g.,
536 # database server crash), not just because no items match the
537 # search criteria.
538 my $sth2 = $dbh->prepare(
539 "SELECT * FROM issues
540 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
541 WHERE itemnumber = ?
545 $sth2->execute( $data->{'itemnumber'} );
546 if ( my $data2 = $sth2->fetchrow_hashref ) {
547 $data->{'date_due'} = $data2->{'date_due'};
548 $data->{'card'} = $data2->{'cardnumber'};
549 $data->{'borrower'} = $data2->{'borrowernumber'};
551 else {
552 $data->{'date_due'} = ($data->{'wthdrawn'} eq '1') ? 'Cancelled' : 'Available';
556 # Find the last 3 people who borrowed this item.
557 $sth2 = $dbh->prepare(
558 "SELECT * FROM old_issues
559 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
560 WHERE itemnumber = ?
561 ORDER BY returndate DESC,timestamp DESC"
564 $sth2->execute( $data->{'itemnumber'} );
565 for ( my $i2 = 0 ; $i2 < 2 ; $i2++ )
566 { # FIXME : error if there is less than 3 pple borrowing this item
567 if ( my $data2 = $sth2->fetchrow_hashref ) {
568 $data->{"timestamp$i2"} = $data2->{'timestamp'};
569 $data->{"card$i2"} = $data2->{'cardnumber'};
570 $data->{"borrower$i2"} = $data2->{'borrowernumber'};
571 } # if
572 } # for
574 $results[$i] = $data;
575 $i++;
578 return (@results);
581 =head2 CanBookBeIssued
583 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
584 $barcode, $duedatespec, $inprocess, $ignore_reserves );
586 Check if a book can be issued.
588 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
590 =over 4
592 =item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
594 =item C<$barcode> is the bar code of the book being issued.
596 =item C<$duedatespec> is a C4::Dates object.
598 =item C<$inprocess> boolean switch
599 =item C<$ignore_reserves> boolean switch
601 =back
603 Returns :
605 =over 4
607 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
608 Possible values are :
610 =back
612 =head3 INVALID_DATE
614 sticky due date is invalid
616 =head3 GNA
618 borrower gone with no address
620 =head3 CARD_LOST
622 borrower declared it's card lost
624 =head3 DEBARRED
626 borrower debarred
628 =head3 UNKNOWN_BARCODE
630 barcode unknown
632 =head3 NOT_FOR_LOAN
634 item is not for loan
636 =head3 WTHDRAWN
638 item withdrawn.
640 =head3 RESTRICTED
642 item is restricted (set by ??)
644 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
645 could be prevented, but ones that can be overriden by the operator.
647 Possible values are :
649 =head3 DEBT
651 borrower has debts.
653 =head3 RENEW_ISSUE
655 renewing, not issuing
657 =head3 ISSUED_TO_ANOTHER
659 issued to someone else.
661 =head3 RESERVED
663 reserved for someone else.
665 =head3 INVALID_DATE
667 sticky due date is invalid or due date in the past
669 =head3 TOO_MANY
671 if the borrower borrows to much things
673 =cut
675 sub CanBookBeIssued {
676 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves ) = @_;
677 my %needsconfirmation; # filled with problems that needs confirmations
678 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
679 my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
680 my $issue = GetItemIssue($item->{itemnumber});
681 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
682 $item->{'itemtype'}=$item->{'itype'};
683 my $dbh = C4::Context->dbh;
685 # MANDATORY CHECKS - unless item exists, nothing else matters
686 unless ( $item->{barcode} ) {
687 $issuingimpossible{UNKNOWN_BARCODE} = 1;
689 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
692 # DUE DATE is OK ? -- should already have checked.
694 unless ( $duedate ) {
695 my $issuedate = strftime( "%Y-%m-%d", localtime );
697 my $branch = _GetCircControlBranch($item,$borrower);
698 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
699 $duedate = CalcDateDue( C4::Dates->new( $issuedate, 'iso' ), $itype, $branch, $borrower );
701 # Offline circ calls AddIssue directly, doesn't run through here
702 # So issuingimpossible should be ok.
704 if ($duedate) {
705 $needsconfirmation{INVALID_DATE} = $duedate->output('syspref')
706 unless $duedate->output('iso') ge C4::Dates->today('iso');
707 } else {
708 $issuingimpossible{INVALID_DATE} = $duedate->output('syspref');
712 # BORROWER STATUS
714 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
715 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
716 &UpdateStats(C4::Context->userenv->{'branch'},'localuse','','',$item->{'itemnumber'},$item->{'itemtype'},$borrower->{'borrowernumber'});
717 ModDateLastSeen( $item->{'itemnumber'} );
718 return( { STATS => 1 }, {});
720 if ( $borrower->{flags}->{GNA} ) {
721 $issuingimpossible{GNA} = 1;
723 if ( $borrower->{flags}->{'LOST'} ) {
724 $issuingimpossible{CARD_LOST} = 1;
726 if ( $borrower->{flags}->{'DBARRED'} ) {
727 $issuingimpossible{DEBARRED} = 1;
729 if ( $borrower->{'dateexpiry'} eq '0000-00-00') {
730 $issuingimpossible{EXPIRED} = 1;
731 } else {
732 my @expirydate= split /-/,$borrower->{'dateexpiry'};
733 if($expirydate[0]==0 || $expirydate[1]==0|| $expirydate[2]==0 ||
734 Date_to_Days(Today) > Date_to_Days( @expirydate )) {
735 $issuingimpossible{EXPIRED} = 1;
739 # BORROWER STATUS
742 # DEBTS
743 my ($amount) =
744 C4::Members::GetMemberAccountRecords( $borrower->{'borrowernumber'}, '' && $duedate->output('iso') );
745 my $amountlimit = C4::Context->preference("noissuescharge");
746 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
747 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
748 if ( C4::Context->preference("IssuingInProcess") ) {
749 if ( $amount > $amountlimit && !$inprocess && !$allowfineoverride) {
750 $issuingimpossible{DEBT} = sprintf( "%.2f", $amount );
751 } elsif ( $amount > $amountlimit && !$inprocess && $allowfineoverride) {
752 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
753 } elsif ( $allfinesneedoverride && $amount > 0 && $amount <= $amountlimit && !$inprocess ) {
754 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
757 else {
758 if ( $amount > $amountlimit && $allowfineoverride ) {
759 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
760 } elsif ( $amount > $amountlimit && !$allowfineoverride) {
761 $issuingimpossible{DEBT} = sprintf( "%.2f", $amount );
762 } elsif ( $amount > 0 && $allfinesneedoverride ) {
763 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
767 my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'});
768 if ($blocktype == -1) {
769 ## patron has outstanding overdue loans
770 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
771 $issuingimpossible{USERBLOCKEDOVERDUE} = $count;
773 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
774 $needsconfirmation{USERBLOCKEDOVERDUE} = $count;
776 } elsif($blocktype == 1) {
777 # patron has accrued fine days
778 $issuingimpossible{USERBLOCKEDREMAINING} = $count;
782 # JB34 CHECKS IF BORROWERS DONT HAVE ISSUE TOO MANY BOOKS
784 my ($current_loan_count, $max_loans_allowed) = TooMany( $borrower, $item->{biblionumber}, $item );
785 # if TooMany max_loans_allowed returns 0 the user doesn't have permission to check out this book
786 if ($max_loans_allowed eq 0) {
787 $needsconfirmation{PATRON_CANT} = 1;
788 } else {
789 if($max_loans_allowed){
790 $needsconfirmation{TOO_MANY} = 1;
791 $needsconfirmation{current_loan_count} = $current_loan_count;
792 $needsconfirmation{max_loans_allowed} = $max_loans_allowed;
797 # ITEM CHECKING
799 if ( $item->{'notforloan'}
800 && $item->{'notforloan'} > 0 )
802 if(!C4::Context->preference("AllowNotForLoanOverride")){
803 $issuingimpossible{NOT_FOR_LOAN} = 1;
804 }else{
805 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
808 elsif ( !$item->{'notforloan'} ){
809 # we have to check itemtypes.notforloan also
810 if (C4::Context->preference('item-level_itypes')){
811 # this should probably be a subroutine
812 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
813 $sth->execute($item->{'itemtype'});
814 my $notforloan=$sth->fetchrow_hashref();
815 $sth->finish();
816 if ($notforloan->{'notforloan'}) {
817 if (!C4::Context->preference("AllowNotForLoanOverride")) {
818 $issuingimpossible{NOT_FOR_LOAN} = 1;
819 } else {
820 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
824 elsif ($biblioitem->{'notforloan'} == 1){
825 if (!C4::Context->preference("AllowNotForLoanOverride")) {
826 $issuingimpossible{NOT_FOR_LOAN} = 1;
827 } else {
828 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
832 if ( $item->{'wthdrawn'} && $item->{'wthdrawn'} > 0 )
834 $issuingimpossible{WTHDRAWN} = 1;
836 if ( $item->{'restricted'}
837 && $item->{'restricted'} == 1 )
839 $issuingimpossible{RESTRICTED} = 1;
841 if ( C4::Context->preference("IndependantBranches") ) {
842 my $userenv = C4::Context->userenv;
843 if ( ($userenv) && ( $userenv->{flags} % 2 != 1 ) ) {
844 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1
845 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} );
846 $needsconfirmation{BORRNOTSAMEBRANCH} = GetBranchName( $borrower->{'branchcode'} )
847 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
852 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
854 if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} )
857 # Already issued to current borrower. Ask whether the loan should
858 # be renewed.
859 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
860 $borrower->{'borrowernumber'},
861 $item->{'itemnumber'}
863 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
864 $issuingimpossible{NO_MORE_RENEWALS} = 1;
866 else {
867 $needsconfirmation{RENEW_ISSUE} = 1;
870 elsif ($issue->{borrowernumber}) {
872 # issued to someone else
873 my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
875 # warn "=>.$currborinfo->{'firstname'} $currborinfo->{'surname'} ($currborinfo->{'cardnumber'})";
876 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
877 $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
878 $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
879 $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
880 $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
883 unless ( $ignore_reserves ) {
884 # See if the item is on reserve.
885 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
886 if ($restype) {
887 my $resbor = $res->{'borrowernumber'};
888 if ( $resbor ne $borrower->{'borrowernumber'} ) {
889 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
890 my $branchname = GetBranchName( $res->{'branchcode'} );
891 if ( $restype eq "Waiting" )
893 # The item is on reserve and waiting, but has been
894 # reserved by some other patron.
895 $needsconfirmation{RESERVE_WAITING} = 1;
896 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
897 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
898 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
899 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
900 $needsconfirmation{'resbranchname'} = $branchname;
901 $needsconfirmation{'reswaitingdate'} = format_date($res->{'waitingdate'});
903 elsif ( $restype eq "Reserved" ) {
904 # The item is on reserve for someone else.
905 $needsconfirmation{RESERVED} = 1;
906 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
907 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
908 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
909 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
910 $needsconfirmation{'resbranchname'} = $branchname;
911 $needsconfirmation{'resreservedate'} = format_date($res->{'reservedate'});
916 return ( \%issuingimpossible, \%needsconfirmation );
919 =head2 AddIssue
921 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
923 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
925 =over 4
927 =item C<$borrower> is a hash with borrower informations (from GetMember or GetMemberDetails).
929 =item C<$barcode> is the barcode of the item being issued.
931 =item C<$datedue> is a C4::Dates object for the max date of return, i.e. the date due (optional).
932 Calculated if empty.
934 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
936 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
937 Defaults to today. Unlike C<$datedue>, NOT a C4::Dates object, unfortunately.
939 AddIssue does the following things :
941 - step 01: check that there is a borrowernumber & a barcode provided
942 - check for RENEWAL (book issued & being issued to the same patron)
943 - renewal YES = Calculate Charge & renew
944 - renewal NO =
945 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
946 * RESERVE PLACED ?
947 - fill reserve if reserve to this patron
948 - cancel reserve or not, otherwise
949 * TRANSFERT PENDING ?
950 - complete the transfert
951 * ISSUE THE BOOK
953 =back
955 =cut
957 sub AddIssue {
958 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode) = @_;
959 my $dbh = C4::Context->dbh;
960 my $barcodecheck=CheckValidBarcode($barcode);
961 # $issuedate defaults to today.
962 if ( ! defined $issuedate ) {
963 $issuedate = strftime( "%Y-%m-%d", localtime );
964 # TODO: for hourly circ, this will need to be a C4::Dates object
965 # and all calls to AddIssue including issuedate will need to pass a Dates object.
967 if ($borrower and $barcode and $barcodecheck ne '0'){
968 # find which item we issue
969 my $item = GetItem('', $barcode) or return undef; # if we don't get an Item, abort.
970 my $branch = _GetCircControlBranch($item,$borrower);
972 # get actual issuing if there is one
973 my $actualissue = GetItemIssue( $item->{itemnumber});
975 # get biblioinformation for this item
976 my $biblio = GetBiblioFromItemNumber($item->{itemnumber});
979 # check if we just renew the issue.
981 if ($actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}) {
982 $datedue = AddRenewal(
983 $borrower->{'borrowernumber'},
984 $item->{'itemnumber'},
985 $branch,
986 $datedue,
987 $issuedate, # here interpreted as the renewal date
990 else {
991 # it's NOT a renewal
992 if ( $actualissue->{borrowernumber}) {
993 # This book is currently on loan, but not to the person
994 # who wants to borrow it now. mark it returned before issuing to the new borrower
995 AddReturn(
996 $item->{'barcode'},
997 C4::Context->userenv->{'branch'}
1001 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1003 # Starting process for transfer job (checking transfert and validate it if we have one)
1004 my ($datesent) = GetTransfers($item->{'itemnumber'});
1005 if ($datesent) {
1006 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1007 my $sth =
1008 $dbh->prepare(
1009 "UPDATE branchtransfers
1010 SET datearrived = now(),
1011 tobranch = ?,
1012 comments = 'Forced branchtransfer'
1013 WHERE itemnumber= ? AND datearrived IS NULL"
1015 $sth->execute(C4::Context->userenv->{'branch'},$item->{'itemnumber'});
1018 # Record in the database the fact that the book was issued.
1019 my $sth =
1020 $dbh->prepare(
1021 "INSERT INTO issues
1022 (borrowernumber, itemnumber,issuedate, date_due, branchcode)
1023 VALUES (?,?,?,?,?)"
1025 unless ($datedue) {
1026 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'};
1027 $datedue = CalcDateDue( C4::Dates->new( $issuedate, 'iso' ), $itype, $branch, $borrower );
1030 $sth->execute(
1031 $borrower->{'borrowernumber'}, # borrowernumber
1032 $item->{'itemnumber'}, # itemnumber
1033 $issuedate, # issuedate
1034 $datedue->output('iso'), # date_due
1035 C4::Context->userenv->{'branch'} # branchcode
1037 $sth->finish;
1038 if ( C4::Context->preference('ReturnToShelvingCart') ) { ## ReturnToShelvingCart is on, anything issued should be taken off the cart.
1039 CartToShelf( $item->{'itemnumber'} );
1041 $item->{'issues'}++;
1043 ## If item was lost, it has now been found, reverse any list item charges if neccessary.
1044 if ( $item->{'itemlost'} ) {
1045 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, $item->{'barcode'} );
1048 ModItem({ issues => $item->{'issues'},
1049 holdingbranch => C4::Context->userenv->{'branch'},
1050 itemlost => 0,
1051 datelastborrowed => C4::Dates->new()->output('iso'),
1052 onloan => $datedue->output('iso'),
1053 }, $item->{'biblionumber'}, $item->{'itemnumber'});
1054 ModDateLastSeen( $item->{'itemnumber'} );
1056 # If it costs to borrow this book, charge it to the patron's account.
1057 my ( $charge, $itemtype ) = GetIssuingCharges(
1058 $item->{'itemnumber'},
1059 $borrower->{'borrowernumber'}
1061 if ( $charge > 0 ) {
1062 AddIssuingCharge(
1063 $item->{'itemnumber'},
1064 $borrower->{'borrowernumber'}, $charge
1066 $item->{'charge'} = $charge;
1069 # Record the fact that this book was issued.
1070 &UpdateStats(
1071 C4::Context->userenv->{'branch'},
1072 'issue', $charge,
1073 ($sipmode ? "SIP-$sipmode" : ''), $item->{'itemnumber'},
1074 $item->{'itype'}, $borrower->{'borrowernumber'}
1077 # Send a checkout slip.
1078 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1079 my %conditions = (
1080 branchcode => $branch,
1081 categorycode => $borrower->{categorycode},
1082 item_type => $item->{itype},
1083 notification => 'CHECKOUT',
1085 if ($circulation_alert->is_enabled_for(\%conditions)) {
1086 SendCirculationAlert({
1087 type => 'CHECKOUT',
1088 item => $item,
1089 borrower => $borrower,
1090 branch => $branch,
1095 logaction("CIRCULATION", "ISSUE", $borrower->{'borrowernumber'}, $biblio->{'biblionumber'})
1096 if C4::Context->preference("IssueLog");
1098 return ($datedue); # not necessarily the same as when it came in!
1101 =head2 GetLoanLength
1103 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1105 Get loan length for an itemtype, a borrower type and a branch
1107 =cut
1109 sub GetLoanLength {
1110 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1111 my $dbh = C4::Context->dbh;
1112 my $sth =
1113 $dbh->prepare(
1114 "select issuelength from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null"
1116 # warn "in get loan lenght $borrowertype $itemtype $branchcode ";
1117 # try to find issuelength & return the 1st available.
1118 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1119 $sth->execute( $borrowertype, $itemtype, $branchcode );
1120 my $loanlength = $sth->fetchrow_hashref;
1121 return $loanlength->{issuelength}
1122 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1124 $sth->execute( $borrowertype, "*", $branchcode );
1125 $loanlength = $sth->fetchrow_hashref;
1126 return $loanlength->{issuelength}
1127 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1129 $sth->execute( "*", $itemtype, $branchcode );
1130 $loanlength = $sth->fetchrow_hashref;
1131 return $loanlength->{issuelength}
1132 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1134 $sth->execute( "*", "*", $branchcode );
1135 $loanlength = $sth->fetchrow_hashref;
1136 return $loanlength->{issuelength}
1137 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1139 $sth->execute( $borrowertype, $itemtype, "*" );
1140 $loanlength = $sth->fetchrow_hashref;
1141 return $loanlength->{issuelength}
1142 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1144 $sth->execute( $borrowertype, "*", "*" );
1145 $loanlength = $sth->fetchrow_hashref;
1146 return $loanlength->{issuelength}
1147 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1149 $sth->execute( "*", $itemtype, "*" );
1150 $loanlength = $sth->fetchrow_hashref;
1151 return $loanlength->{issuelength}
1152 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1154 $sth->execute( "*", "*", "*" );
1155 $loanlength = $sth->fetchrow_hashref;
1156 return $loanlength->{issuelength}
1157 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1159 # if no rule is set => 21 days (hardcoded)
1160 return 21;
1164 =head2 GetHardDueDate
1166 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1168 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1170 =cut
1172 sub GetHardDueDate {
1173 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1174 my $dbh = C4::Context->dbh;
1175 my $sth =
1176 $dbh->prepare(
1177 "select hardduedate, hardduedatecompare from issuingrules where categorycode=? and itemtype=? and branchcode=?"
1179 $sth->execute( $borrowertype, $itemtype, $branchcode );
1180 my $results = $sth->fetchrow_hashref;
1181 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1182 if defined($results) && $results->{hardduedate} ne 'NULL';
1184 $sth->execute( $borrowertype, "*", $branchcode );
1185 $results = $sth->fetchrow_hashref;
1186 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1187 if defined($results) && $results->{hardduedate} ne 'NULL';
1189 $sth->execute( "*", $itemtype, $branchcode );
1190 $results = $sth->fetchrow_hashref;
1191 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1192 if defined($results) && $results->{hardduedate} ne 'NULL';
1194 $sth->execute( "*", "*", $branchcode );
1195 $results = $sth->fetchrow_hashref;
1196 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1197 if defined($results) && $results->{hardduedate} ne 'NULL';
1199 $sth->execute( $borrowertype, $itemtype, "*" );
1200 $results = $sth->fetchrow_hashref;
1201 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1202 if defined($results) && $results->{hardduedate} ne 'NULL';
1204 $sth->execute( $borrowertype, "*", "*" );
1205 $results = $sth->fetchrow_hashref;
1206 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1207 if defined($results) && $results->{hardduedate} ne 'NULL';
1209 $sth->execute( "*", $itemtype, "*" );
1210 $results = $sth->fetchrow_hashref;
1211 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1212 if defined($results) && $results->{hardduedate} ne 'NULL';
1214 $sth->execute( "*", "*", "*" );
1215 $results = $sth->fetchrow_hashref;
1216 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1217 if defined($results) && $results->{hardduedate} ne 'NULL';
1219 # if no rule is set => return undefined
1220 return (undef, undef);
1223 =head2 GetIssuingRule
1225 my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
1227 FIXME - This is a copy-paste of GetLoanLength
1228 as a stop-gap. Do not wish to change API for GetLoanLength
1229 this close to release, however, Overdues::GetIssuingRules is broken.
1231 Get the issuing rule for an itemtype, a borrower type and a branch
1232 Returns a hashref from the issuingrules table.
1234 =cut
1236 sub GetIssuingRule {
1237 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1238 my $dbh = C4::Context->dbh;
1239 my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null" );
1240 my $irule;
1242 $sth->execute( $borrowertype, $itemtype, $branchcode );
1243 $irule = $sth->fetchrow_hashref;
1244 return $irule if defined($irule) ;
1246 $sth->execute( $borrowertype, "*", $branchcode );
1247 $irule = $sth->fetchrow_hashref;
1248 return $irule if defined($irule) ;
1250 $sth->execute( "*", $itemtype, $branchcode );
1251 $irule = $sth->fetchrow_hashref;
1252 return $irule if defined($irule) ;
1254 $sth->execute( "*", "*", $branchcode );
1255 $irule = $sth->fetchrow_hashref;
1256 return $irule if defined($irule) ;
1258 $sth->execute( $borrowertype, $itemtype, "*" );
1259 $irule = $sth->fetchrow_hashref;
1260 return $irule if defined($irule) ;
1262 $sth->execute( $borrowertype, "*", "*" );
1263 $irule = $sth->fetchrow_hashref;
1264 return $irule if defined($irule) ;
1266 $sth->execute( "*", $itemtype, "*" );
1267 $irule = $sth->fetchrow_hashref;
1268 return $irule if defined($irule) ;
1270 $sth->execute( "*", "*", "*" );
1271 $irule = $sth->fetchrow_hashref;
1272 return $irule if defined($irule) ;
1274 # if no rule matches,
1275 return undef;
1278 =head2 GetBranchBorrowerCircRule
1280 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1282 Retrieves circulation rule attributes that apply to the given
1283 branch and patron category, regardless of item type.
1284 The return value is a hashref containing the following key:
1286 maxissueqty - maximum number of loans that a
1287 patron of the given category can have at the given
1288 branch. If the value is undef, no limit.
1290 This will first check for a specific branch and
1291 category match from branch_borrower_circ_rules.
1293 If no rule is found, it will then check default_branch_circ_rules
1294 (same branch, default category). If no rule is found,
1295 it will then check default_borrower_circ_rules (default
1296 branch, same category), then failing that, default_circ_rules
1297 (default branch, default category).
1299 If no rule has been found in the database, it will default to
1300 the buillt in rule:
1302 maxissueqty - undef
1304 C<$branchcode> and C<$categorycode> should contain the
1305 literal branch code and patron category code, respectively - no
1306 wildcards.
1308 =cut
1310 sub GetBranchBorrowerCircRule {
1311 my $branchcode = shift;
1312 my $categorycode = shift;
1314 my $branch_cat_query = "SELECT maxissueqty
1315 FROM branch_borrower_circ_rules
1316 WHERE branchcode = ?
1317 AND categorycode = ?";
1318 my $dbh = C4::Context->dbh();
1319 my $sth = $dbh->prepare($branch_cat_query);
1320 $sth->execute($branchcode, $categorycode);
1321 my $result;
1322 if ($result = $sth->fetchrow_hashref()) {
1323 return $result;
1326 # try same branch, default borrower category
1327 my $branch_query = "SELECT maxissueqty
1328 FROM default_branch_circ_rules
1329 WHERE branchcode = ?";
1330 $sth = $dbh->prepare($branch_query);
1331 $sth->execute($branchcode);
1332 if ($result = $sth->fetchrow_hashref()) {
1333 return $result;
1336 # try default branch, same borrower category
1337 my $category_query = "SELECT maxissueqty
1338 FROM default_borrower_circ_rules
1339 WHERE categorycode = ?";
1340 $sth = $dbh->prepare($category_query);
1341 $sth->execute($categorycode);
1342 if ($result = $sth->fetchrow_hashref()) {
1343 return $result;
1346 # try default branch, default borrower category
1347 my $default_query = "SELECT maxissueqty
1348 FROM default_circ_rules";
1349 $sth = $dbh->prepare($default_query);
1350 $sth->execute();
1351 if ($result = $sth->fetchrow_hashref()) {
1352 return $result;
1355 # built-in default circulation rule
1356 return {
1357 maxissueqty => undef,
1361 =head2 GetBranchItemRule
1363 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1365 Retrieves circulation rule attributes that apply to the given
1366 branch and item type, regardless of patron category.
1368 The return value is a hashref containing the following keys:
1370 holdallowed => Hold policy for this branch and itemtype. Possible values:
1371 0: No holds allowed.
1372 1: Holds allowed only by patrons that have the same homebranch as the item.
1373 2: Holds allowed from any patron.
1375 returnbranch => branch to which to return item. Possible values:
1376 noreturn: do not return, let item remain where checked in (floating collections)
1377 homebranch: return to item's home branch
1379 This searches branchitemrules in the following order:
1381 * Same branchcode and itemtype
1382 * Same branchcode, itemtype '*'
1383 * branchcode '*', same itemtype
1384 * branchcode and itemtype '*'
1386 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1388 =cut
1390 sub GetBranchItemRule {
1391 my ( $branchcode, $itemtype ) = @_;
1392 my $dbh = C4::Context->dbh();
1393 my $result = {};
1395 my @attempts = (
1396 ['SELECT holdallowed, returnbranch
1397 FROM branch_item_rules
1398 WHERE branchcode = ?
1399 AND itemtype = ?', $branchcode, $itemtype],
1400 ['SELECT holdallowed, returnbranch
1401 FROM default_branch_circ_rules
1402 WHERE branchcode = ?', $branchcode],
1403 ['SELECT holdallowed, returnbranch
1404 FROM default_branch_item_rules
1405 WHERE itemtype = ?', $itemtype],
1406 ['SELECT holdallowed, returnbranch
1407 FROM default_circ_rules'],
1410 foreach my $attempt (@attempts) {
1411 my ($query, @bind_params) = @{$attempt};
1412 my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params );
1414 # Since branch/category and branch/itemtype use the same per-branch
1415 # defaults tables, we have to check that the key we want is set, not
1416 # just that a row was returned
1417 $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
1418 $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1421 # built-in default circulation rule
1422 $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1423 $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1425 return $result;
1428 =head2 AddReturn
1430 ($doreturn, $messages, $iteminformation, $borrower) =
1431 &AddReturn($barcode, $branch, $exemptfine, $dropbox);
1433 Returns a book.
1435 =over 4
1437 =item C<$barcode> is the bar code of the book being returned.
1439 =item C<$branch> is the code of the branch where the book is being returned.
1441 =item C<$exemptfine> indicates that overdue charges for the item will be
1442 removed.
1444 =item C<$dropbox> indicates that the check-in date is assumed to be
1445 yesterday, or the last non-holiday as defined in C4::Calendar . If
1446 overdue charges are applied and C<$dropbox> is true, the last charge
1447 will be removed. This assumes that the fines accrual script has run
1448 for _today_.
1450 =back
1452 C<&AddReturn> returns a list of four items:
1454 C<$doreturn> is true iff the return succeeded.
1456 C<$messages> is a reference-to-hash giving feedback on the operation.
1457 The keys of the hash are:
1459 =over 4
1461 =item C<BadBarcode>
1463 No item with this barcode exists. The value is C<$barcode>.
1465 =item C<NotIssued>
1467 The book is not currently on loan. The value is C<$barcode>.
1469 =item C<IsPermanent>
1471 The book's home branch is a permanent collection. If you have borrowed
1472 this book, you are not allowed to return it. The value is the code for
1473 the book's home branch.
1475 =item C<wthdrawn>
1477 This book has been withdrawn/cancelled. The value should be ignored.
1479 =item C<Wrongbranch>
1481 This book has was returned to the wrong branch. The value is a hashref
1482 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1483 contain the branchcode of the incorrect and correct return library, respectively.
1485 =item C<ResFound>
1487 The item was reserved. The value is a reference-to-hash whose keys are
1488 fields from the reserves table of the Koha database, and
1489 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1490 either C<Waiting>, C<Reserved>, or 0.
1492 =back
1494 C<$iteminformation> is a reference-to-hash, giving information about the
1495 returned item from the issues table.
1497 C<$borrower> is a reference-to-hash, giving information about the
1498 patron who last borrowed the book.
1500 =cut
1502 sub AddReturn {
1503 my ( $barcode, $branch, $exemptfine, $dropbox ) = @_;
1504 if ($branch and not GetBranchDetail($branch)) {
1505 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1506 undef $branch;
1508 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1509 my $messages;
1510 my $borrower;
1511 my $biblio;
1512 my $doreturn = 1;
1513 my $validTransfert = 0;
1514 my $stat_type = 'return';
1516 # get information on item
1517 my $itemnumber = GetItemnumberFromBarcode( $barcode );
1518 unless ($itemnumber) {
1519 return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out.
1521 my $issue = GetItemIssue($itemnumber);
1522 # warn Dumper($iteminformation);
1523 if ($issue and $issue->{borrowernumber}) {
1524 $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1525 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n"
1526 . Dumper($issue) . "\n";
1527 } else {
1528 $messages->{'NotIssued'} = $barcode;
1529 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1530 $doreturn = 0;
1531 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1532 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1533 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1534 $messages->{'LocalUse'} = 1;
1535 $stat_type = 'localuse';
1539 my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1540 # full item data, but no borrowernumber or checkout info (no issue)
1541 # we know GetItem should work because GetItemnumberFromBarcode worked
1542 my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1543 # get the proper branch to which to return the item
1544 $hbr = $item->{$hbr} || $branch ;
1545 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1547 my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
1549 # check if the book is in a permanent collection....
1550 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1551 if ( $hbr ) {
1552 my $branches = GetBranches(); # a potentially expensive call for a non-feature.
1553 $branches->{$hbr}->{PE} and $messages->{'IsPermanent'} = $hbr;
1556 # if indy branches and returning to different branch, refuse the return
1557 if ($hbr ne $branch && C4::Context->preference("IndependantBranches")){
1558 $messages->{'Wrongbranch'} = {
1559 Wrongbranch => $branch,
1560 Rightbranch => $hbr,
1562 $doreturn = 0;
1563 # bailing out here - in this case, current desired behavior
1564 # is to act as if no return ever happened at all.
1565 # FIXME - even in an indy branches situation, there should
1566 # still be an option for the library to accept the item
1567 # and transfer it to its owning library.
1568 return ( $doreturn, $messages, $issue, $borrower );
1571 if ( $item->{'wthdrawn'} ) { # book has been cancelled
1572 $messages->{'wthdrawn'} = 1;
1573 $doreturn = 0;
1576 # case of a return of document (deal with issues and holdingbranch)
1577 if ($doreturn) {
1578 $borrower or warn "AddReturn without current borrower";
1579 my $circControlBranch;
1580 if ($dropbox) {
1581 # define circControlBranch only if dropbox mode is set
1582 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1583 # FIXME: check issuedate > returndate, factoring in holidays
1584 $circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );;
1587 if ($borrowernumber) {
1588 MarkIssueReturned($borrowernumber, $item->{'itemnumber'}, $circControlBranch, '', $borrower->{'privacy'});
1589 $messages->{'WasReturned'} = 1; # FIXME is the "= 1" right? This could be the borrower hash.
1592 ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1595 # the holdingbranch is updated if the document is returned to another location.
1596 # this is always done regardless of whether the item was on loan or not
1597 if ($item->{'holdingbranch'} ne $branch) {
1598 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1599 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1601 ModDateLastSeen( $item->{'itemnumber'} );
1603 # check if we have a transfer for this document
1604 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1606 # if we have a transfer to do, we update the line of transfers with the datearrived
1607 if ($datesent) {
1608 if ( $tobranch eq $branch ) {
1609 my $sth = C4::Context->dbh->prepare(
1610 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1612 $sth->execute( $item->{'itemnumber'} );
1613 # if we have a reservation with valid transfer, we can set it's status to 'W'
1614 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1615 } else {
1616 $messages->{'WrongTransfer'} = $tobranch;
1617 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1619 $validTransfert = 1;
1622 # fix up the accounts.....
1623 if ($item->{'itemlost'}) {
1624 _FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode); # can tolerate undef $borrowernumber
1625 $messages->{'WasLost'} = 1;
1628 # fix up the overdues in accounts...
1629 if ($borrowernumber) {
1630 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
1631 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
1633 # fix fine days
1634 my $debardate = _FixFineDaysOnReturn( $borrower, $item, $issue->{date_due} );
1635 $messages->{'Debarred'} = $debardate if ($debardate);
1638 # find reserves.....
1639 # if we don't have a reserve with the status W, we launch the Checkreserves routine
1640 my ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
1641 if ($resfound) {
1642 $resrec->{'ResFound'} = $resfound;
1643 $messages->{'ResFound'} = $resrec;
1646 # update stats?
1647 # Record the fact that this book was returned.
1648 UpdateStats(
1649 $branch, $stat_type, '0', '',
1650 $item->{'itemnumber'},
1651 $biblio->{'itemtype'},
1652 $borrowernumber
1655 # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
1656 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1657 my %conditions = (
1658 branchcode => $branch,
1659 categorycode => $borrower->{categorycode},
1660 item_type => $item->{itype},
1661 notification => 'CHECKIN',
1663 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
1664 SendCirculationAlert({
1665 type => 'CHECKIN',
1666 item => $item,
1667 borrower => $borrower,
1668 branch => $branch,
1672 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'biblionumber'})
1673 if C4::Context->preference("ReturnLog");
1675 # FIXME: make this comment intelligible.
1676 #adding message if holdingbranch is non equal a userenv branch to return the document to homebranch
1677 #we check, if we don't have reserv or transfert for this document, if not, return it to homebranch .
1679 if (($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $hbr) and not $messages->{'WrongTransfer'}){
1680 if ( C4::Context->preference("AutomaticItemReturn" ) or
1681 (C4::Context->preference("UseBranchTransferLimits") and
1682 ! IsBranchTransferAllowed($branch, $hbr, $item->{C4::Context->preference("BranchTransferLimitsType")} )
1683 )) {
1684 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $hbr;
1685 $debug and warn "item: " . Dumper($item);
1686 ModItemTransfer($item->{'itemnumber'}, $branch, $hbr);
1687 $messages->{'WasTransfered'} = 1;
1688 } else {
1689 $messages->{'NeedsTransfer'} = 1; # TODO: instead of 1, specify branchcode that the transfer SHOULD go to, $item->{homebranch}
1692 return ( $doreturn, $messages, $issue, $borrower );
1695 =head2 MarkIssueReturned
1697 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
1699 Unconditionally marks an issue as being returned by
1700 moving the C<issues> row to C<old_issues> and
1701 setting C<returndate> to the current date, or
1702 the last non-holiday date of the branccode specified in
1703 C<dropbox_branch> . Assumes you've already checked that
1704 it's safe to do this, i.e. last non-holiday > issuedate.
1706 if C<$returndate> is specified (in iso format), it is used as the date
1707 of the return. It is ignored when a dropbox_branch is passed in.
1709 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
1710 the old_issue is immediately anonymised
1712 Ideally, this function would be internal to C<C4::Circulation>,
1713 not exported, but it is currently needed by one
1714 routine in C<C4::Accounts>.
1716 =cut
1718 sub MarkIssueReturned {
1719 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
1720 my $dbh = C4::Context->dbh;
1721 my $query = "UPDATE issues SET returndate=";
1722 my @bind;
1723 if ($dropbox_branch) {
1724 my $calendar = C4::Calendar->new( branchcode => $dropbox_branch );
1725 my $dropboxdate = $calendar->addDate( C4::Dates->new(), -1 );
1726 $query .= " ? ";
1727 push @bind, $dropboxdate->output('iso');
1728 } elsif ($returndate) {
1729 $query .= " ? ";
1730 push @bind, $returndate;
1731 } else {
1732 $query .= " now() ";
1734 $query .= " WHERE borrowernumber = ? AND itemnumber = ?";
1735 push @bind, $borrowernumber, $itemnumber;
1736 # FIXME transaction
1737 my $sth_upd = $dbh->prepare($query);
1738 $sth_upd->execute(@bind);
1739 my $sth_copy = $dbh->prepare("INSERT INTO old_issues SELECT * FROM issues
1740 WHERE borrowernumber = ?
1741 AND itemnumber = ?");
1742 $sth_copy->execute($borrowernumber, $itemnumber);
1743 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
1744 if ( $privacy == 2) {
1745 # The default of 0 does not work due to foreign key constraints
1746 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
1747 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
1748 my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
1749 WHERE borrowernumber = ?
1750 AND itemnumber = ?");
1751 $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
1753 my $sth_del = $dbh->prepare("DELETE FROM issues
1754 WHERE borrowernumber = ?
1755 AND itemnumber = ?");
1756 $sth_del->execute($borrowernumber, $itemnumber);
1759 =head2 _FixFineDaysOnReturn
1761 &_FixFineDaysOnReturn($borrower, $item, $datedue);
1763 C<$borrower> borrower hashref
1765 C<$item> item hashref
1767 C<$datedue> date due
1769 Internal function, called only by AddReturn that calculate and update the user fine days, and debars him
1771 =cut
1773 sub _FixFineDaysOnReturn {
1774 my ( $borrower, $item, $datedue ) = @_;
1776 if ($datedue) {
1777 $datedue = C4::Dates->new( $datedue, "iso" );
1778 } else {
1779 return;
1782 my $branchcode = _GetCircControlBranch( $item, $borrower );
1783 my $calendar = C4::Calendar->new( branchcode => $branchcode );
1784 my $today = C4::Dates->new();
1786 my $deltadays = $calendar->daysBetween( $datedue, C4::Dates->new() );
1788 my $circcontrol = C4::Context::preference('CircControl');
1789 my $issuingrule = GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
1790 my $finedays = $issuingrule->{finedays};
1792 # exit if no finedays defined
1793 return unless $finedays;
1794 my $grace = $issuingrule->{firstremind};
1796 if ( $deltadays - $grace > 0 ) {
1797 my @newdate = Add_Delta_Days( Today(), $deltadays * $finedays );
1798 my $isonewdate = join( '-', @newdate );
1799 my ( $deby, $debm, $debd ) = split( /-/, $borrower->{debarred} );
1800 if ( check_date( $deby, $debm, $debd ) ) {
1801 my @olddate = split( /-/, $borrower->{debarred} );
1803 if ( Delta_Days( @olddate, @newdate ) > 0 ) {
1804 C4::Members::DebarMember( $borrower->{borrowernumber}, $isonewdate );
1805 return $isonewdate;
1807 } else {
1808 C4::Members::DebarMember( $borrower->{borrowernumber}, $isonewdate );
1809 return $isonewdate;
1814 =head2 _FixOverduesOnReturn
1816 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
1818 C<$brn> borrowernumber
1820 C<$itm> itemnumber
1822 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
1823 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
1825 Internal function, called only by AddReturn
1827 =cut
1829 sub _FixOverduesOnReturn {
1830 my ($borrowernumber, $item);
1831 unless ($borrowernumber = shift) {
1832 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
1833 return;
1835 unless ($item = shift) {
1836 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
1837 return;
1839 my ($exemptfine, $dropbox) = @_;
1840 my $dbh = C4::Context->dbh;
1842 # check for overdue fine
1843 my $sth = $dbh->prepare(
1844 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
1846 $sth->execute( $borrowernumber, $item );
1848 # alter fine to show that the book has been returned
1849 my $data = $sth->fetchrow_hashref;
1850 return 0 unless $data; # no warning, there's just nothing to fix
1852 my $uquery;
1853 my @bind = ($borrowernumber, $item, $data->{'accountno'});
1854 if ($exemptfine) {
1855 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
1856 if (C4::Context->preference("FinesLog")) {
1857 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
1859 } elsif ($dropbox && $data->{lastincrement}) {
1860 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
1861 my $amt = $data->{amount} - $data->{lastincrement} ;
1862 if (C4::Context->preference("FinesLog")) {
1863 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
1865 $uquery = "update accountlines set accounttype='F' ";
1866 if($outstanding >= 0 && $amt >=0) {
1867 $uquery .= ", amount = ? , amountoutstanding=? ";
1868 unshift @bind, ($amt, $outstanding) ;
1870 } else {
1871 $uquery = "update accountlines set accounttype='F' ";
1873 $uquery .= " where (borrowernumber = ?) and (itemnumber = ?) and (accountno = ?)";
1874 my $usth = $dbh->prepare($uquery);
1875 return $usth->execute(@bind);
1878 =head2 _FixAccountForLostAndReturned
1880 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
1882 Calculates the charge for a book lost and returned.
1884 Internal function, not exported, called only by AddReturn.
1886 FIXME: This function reflects how inscrutable fines logic is. Fix both.
1887 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
1889 =cut
1891 sub _FixAccountForLostAndReturned {
1892 my $itemnumber = shift or return;
1893 my $borrowernumber = @_ ? shift : undef;
1894 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
1895 my $dbh = C4::Context->dbh;
1896 # check for charge made for lost book
1897 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
1898 $sth->execute($itemnumber);
1899 my $data = $sth->fetchrow_hashref;
1900 $data or return; # bail if there is nothing to do
1901 $data->{accounttype} eq 'W' and return; # Written off
1903 # writeoff this amount
1904 my $offset;
1905 my $amount = $data->{'amount'};
1906 my $acctno = $data->{'accountno'};
1907 my $amountleft; # Starts off undef/zero.
1908 if ($data->{'amountoutstanding'} == $amount) {
1909 $offset = $data->{'amount'};
1910 $amountleft = 0; # Hey, it's zero here, too.
1911 } else {
1912 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
1913 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
1915 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
1916 WHERE (borrowernumber = ?)
1917 AND (itemnumber = ?) AND (accountno = ?) ");
1918 $usth->execute($data->{'borrowernumber'},$itemnumber,$acctno); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
1919 #check if any credit is left if so writeoff other accounts
1920 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
1921 $amountleft *= -1 if ($amountleft < 0);
1922 if ($amountleft > 0) {
1923 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
1924 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
1925 $msth->execute($data->{'borrowernumber'});
1926 # offset transactions
1927 my $newamtos;
1928 my $accdata;
1929 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
1930 if ($accdata->{'amountoutstanding'} < $amountleft) {
1931 $newamtos = 0;
1932 $amountleft -= $accdata->{'amountoutstanding'};
1933 } else {
1934 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
1935 $amountleft = 0;
1937 my $thisacct = $accdata->{'accountno'};
1938 # FIXME: move prepares outside while loop!
1939 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
1940 WHERE (borrowernumber = ?)
1941 AND (accountno=?)");
1942 $usth->execute($newamtos,$data->{'borrowernumber'},'$thisacct'); # FIXME: '$thisacct' is a string literal!
1943 $usth = $dbh->prepare("INSERT INTO accountoffsets
1944 (borrowernumber, accountno, offsetaccount, offsetamount)
1945 VALUES
1946 (?,?,?,?)");
1947 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
1949 $msth->finish; # $msth might actually have data left
1951 $amountleft *= -1 if ($amountleft > 0);
1952 my $desc = "Item Returned " . $item_id;
1953 $usth = $dbh->prepare("INSERT INTO accountlines
1954 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
1955 VALUES (?,?,now(),?,?,'CR',?)");
1956 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
1957 if ($borrowernumber) {
1958 # FIXME: same as query above. use 1 sth for both
1959 $usth = $dbh->prepare("INSERT INTO accountoffsets
1960 (borrowernumber, accountno, offsetaccount, offsetamount)
1961 VALUES (?,?,?,?)");
1962 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
1964 ModItem({ paidfor => '' }, undef, $itemnumber);
1965 return;
1968 =head2 _GetCircControlBranch
1970 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
1972 Internal function :
1974 Return the library code to be used to determine which circulation
1975 policy applies to a transaction. Looks up the CircControl and
1976 HomeOrHoldingBranch system preferences.
1978 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
1980 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
1982 =cut
1984 sub _GetCircControlBranch {
1985 my ($item, $borrower) = @_;
1986 my $circcontrol = C4::Context->preference('CircControl');
1987 my $branch;
1989 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
1990 $branch= C4::Context->userenv->{'branch'};
1991 } elsif ($circcontrol eq 'PatronLibrary') {
1992 $branch=$borrower->{branchcode};
1993 } else {
1994 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
1995 $branch = $item->{$branchfield};
1996 # default to item home branch if holdingbranch is used
1997 # and is not defined
1998 if (!defined($branch) && $branchfield eq 'holdingbranch') {
1999 $branch = $item->{homebranch};
2002 return $branch;
2010 =head2 GetItemIssue
2012 $issue = &GetItemIssue($itemnumber);
2014 Returns patron currently having a book, or undef if not checked out.
2016 C<$itemnumber> is the itemnumber.
2018 C<$issue> is a hashref of the row from the issues table.
2020 =cut
2022 sub GetItemIssue {
2023 my ($itemnumber) = @_;
2024 return unless $itemnumber;
2025 my $sth = C4::Context->dbh->prepare(
2026 "SELECT *
2027 FROM issues
2028 LEFT JOIN items ON issues.itemnumber=items.itemnumber
2029 WHERE issues.itemnumber=?");
2030 $sth->execute($itemnumber);
2031 my $data = $sth->fetchrow_hashref;
2032 return unless $data;
2033 $data->{'overdue'} = ($data->{'date_due'} lt C4::Dates->today('iso')) ? 1 : 0;
2034 return ($data);
2037 =head2 GetOpenIssue
2039 $issue = GetOpenIssue( $itemnumber );
2041 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2043 C<$itemnumber> is the item's itemnumber
2045 Returns a hashref
2047 =cut
2049 sub GetOpenIssue {
2050 my ( $itemnumber ) = @_;
2052 my $dbh = C4::Context->dbh;
2053 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2054 $sth->execute( $itemnumber );
2055 my $issue = $sth->fetchrow_hashref();
2056 return $issue;
2059 =head2 GetItemIssues
2061 $issues = &GetItemIssues($itemnumber, $history);
2063 Returns patrons that have issued a book
2065 C<$itemnumber> is the itemnumber
2066 C<$history> is false if you just want the current "issuer" (if any)
2067 and true if you want issues history from old_issues also.
2069 Returns reference to an array of hashes
2071 =cut
2073 sub GetItemIssues {
2074 my ( $itemnumber, $history ) = @_;
2076 my $today = C4::Dates->today('iso'); # get today date
2077 my $sql = "SELECT * FROM issues
2078 JOIN borrowers USING (borrowernumber)
2079 JOIN items USING (itemnumber)
2080 WHERE issues.itemnumber = ? ";
2081 if ($history) {
2082 $sql .= "UNION ALL
2083 SELECT * FROM old_issues
2084 LEFT JOIN borrowers USING (borrowernumber)
2085 JOIN items USING (itemnumber)
2086 WHERE old_issues.itemnumber = ? ";
2088 $sql .= "ORDER BY date_due DESC";
2089 my $sth = C4::Context->dbh->prepare($sql);
2090 if ($history) {
2091 $sth->execute($itemnumber, $itemnumber);
2092 } else {
2093 $sth->execute($itemnumber);
2095 my $results = $sth->fetchall_arrayref({});
2096 foreach (@$results) {
2097 $_->{'overdue'} = ($_->{'date_due'} lt $today) ? 1 : 0;
2099 return $results;
2102 =head2 GetBiblioIssues
2104 $issues = GetBiblioIssues($biblionumber);
2106 this function get all issues from a biblionumber.
2108 Return:
2109 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2110 tables issues and the firstname,surname & cardnumber from borrowers.
2112 =cut
2114 sub GetBiblioIssues {
2115 my $biblionumber = shift;
2116 return undef unless $biblionumber;
2117 my $dbh = C4::Context->dbh;
2118 my $query = "
2119 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2120 FROM issues
2121 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2122 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2123 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2124 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2125 WHERE biblio.biblionumber = ?
2126 UNION ALL
2127 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2128 FROM old_issues
2129 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2130 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2131 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2132 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2133 WHERE biblio.biblionumber = ?
2134 ORDER BY timestamp
2136 my $sth = $dbh->prepare($query);
2137 $sth->execute($biblionumber, $biblionumber);
2139 my @issues;
2140 while ( my $data = $sth->fetchrow_hashref ) {
2141 push @issues, $data;
2143 return \@issues;
2146 =head2 GetUpcomingDueIssues
2148 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2150 =cut
2152 sub GetUpcomingDueIssues {
2153 my $params = shift;
2155 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2156 my $dbh = C4::Context->dbh;
2158 my $statement = <<END_SQL;
2159 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2160 FROM issues
2161 LEFT JOIN items USING (itemnumber)
2162 LEFT OUTER JOIN branches USING (branchcode)
2163 WhERE returndate is NULL
2164 AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ?
2165 END_SQL
2167 my @bind_parameters = ( $params->{'days_in_advance'} );
2169 my $sth = $dbh->prepare( $statement );
2170 $sth->execute( @bind_parameters );
2171 my $upcoming_dues = $sth->fetchall_arrayref({});
2172 $sth->finish;
2174 return $upcoming_dues;
2177 =head2 CanBookBeRenewed
2179 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2181 Find out whether a borrowed item may be renewed.
2183 C<$dbh> is a DBI handle to the Koha database.
2185 C<$borrowernumber> is the borrower number of the patron who currently
2186 has the item on loan.
2188 C<$itemnumber> is the number of the item to renew.
2190 C<$override_limit>, if supplied with a true value, causes
2191 the limit on the number of times that the loan can be renewed
2192 (as controlled by the item type) to be ignored.
2194 C<$CanBookBeRenewed> returns a true value iff the item may be renewed. The
2195 item must currently be on loan to the specified borrower; renewals
2196 must be allowed for the item's type; and the borrower must not have
2197 already renewed the loan. $error will contain the reason the renewal can not proceed
2199 =cut
2201 sub CanBookBeRenewed {
2203 # check renewal status
2204 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2205 my $dbh = C4::Context->dbh;
2206 my $renews = 1;
2207 my $renewokay = 0;
2208 my $error;
2210 # Look in the issues table for this item, lent to this borrower,
2211 # and not yet returned.
2213 # Look in the issues table for this item, lent to this borrower,
2214 # and not yet returned.
2215 my %branch = (
2216 'ItemHomeLibrary' => 'items.homebranch',
2217 'PickupLibrary' => 'items.holdingbranch',
2218 'PatronLibrary' => 'borrowers.branchcode'
2220 my $controlbranch = $branch{C4::Context->preference('CircControl')};
2221 my $itype = C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype';
2223 my $sthcount = $dbh->prepare("
2224 SELECT
2225 borrowers.categorycode, biblioitems.itemtype, issues.renewals, renewalsallowed, $controlbranch
2226 FROM issuingrules,
2227 issues
2228 LEFT JOIN items USING (itemnumber)
2229 LEFT JOIN borrowers USING (borrowernumber)
2230 LEFT JOIN biblioitems USING (biblioitemnumber)
2232 WHERE
2233 (issuingrules.categorycode = borrowers.categorycode OR issuingrules.categorycode = '*')
2235 (issuingrules.itemtype = $itype OR issuingrules.itemtype = '*')
2237 (issuingrules.branchcode = $controlbranch OR issuingrules.branchcode = '*')
2238 AND
2239 borrowernumber = ?
2241 itemnumber = ?
2242 ORDER BY
2243 issuingrules.categorycode desc,
2244 issuingrules.itemtype desc,
2245 issuingrules.branchcode desc
2246 LIMIT 1;
2249 $sthcount->execute( $borrowernumber, $itemnumber );
2250 if ( my $data1 = $sthcount->fetchrow_hashref ) {
2252 if ( ( $data1->{renewalsallowed} && $data1->{renewalsallowed} > $data1->{renewals} ) || $override_limit ) {
2253 $renewokay = 1;
2255 else {
2256 $error="too_many";
2259 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2260 if ($resfound) {
2261 $renewokay = 0;
2262 $error="on_reserve"
2266 return ($renewokay,$error);
2269 =head2 AddRenewal
2271 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2273 Renews a loan.
2275 C<$borrowernumber> is the borrower number of the patron who currently
2276 has the item.
2278 C<$itemnumber> is the number of the item to renew.
2280 C<$branch> is the library where the renewal took place (if any).
2281 The library that controls the circ policies for the renewal is retrieved from the issues record.
2283 C<$datedue> can be a C4::Dates object used to set the due date.
2285 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2286 this parameter is not supplied, lastreneweddate is set to the current date.
2288 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2289 from the book's item type.
2291 =cut
2293 sub AddRenewal {
2294 my $borrowernumber = shift or return undef;
2295 my $itemnumber = shift or return undef;
2296 my $branch = shift;
2297 my $datedue = shift;
2298 my $lastreneweddate = shift || C4::Dates->new()->output('iso');
2299 my $item = GetItem($itemnumber) or return undef;
2300 my $biblio = GetBiblioFromItemNumber($itemnumber) or return undef;
2302 my $dbh = C4::Context->dbh;
2303 # Find the issues record for this book
2304 my $sth =
2305 $dbh->prepare("SELECT * FROM issues
2306 WHERE borrowernumber=?
2307 AND itemnumber=?"
2309 $sth->execute( $borrowernumber, $itemnumber );
2310 my $issuedata = $sth->fetchrow_hashref;
2311 $sth->finish;
2312 if($datedue && ! $datedue->output('iso')){
2313 warn "Invalid date passed to AddRenewal.";
2314 return undef;
2316 # If the due date wasn't specified, calculate it by adding the
2317 # book's loan length to today's date or the current due date
2318 # based on the value of the RenewalPeriodBase syspref.
2319 unless ($datedue) {
2321 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return undef;
2322 my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2324 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2325 C4::Dates->new($issuedata->{date_due}, 'iso') :
2326 C4::Dates->new();
2327 $datedue = CalcDateDue($datedue,$itemtype,$issuedata->{'branchcode'},$borrower);
2330 # Update the issues record to have the new due date, and a new count
2331 # of how many times it has been renewed.
2332 my $renews = $issuedata->{'renewals'} + 1;
2333 $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2334 WHERE borrowernumber=?
2335 AND itemnumber=?"
2337 $sth->execute( $datedue->output('iso'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2338 $sth->finish;
2340 # Update the renewal count on the item, and tell zebra to reindex
2341 $renews = $biblio->{'renewals'} + 1;
2342 ModItem({ renewals => $renews, onloan => $datedue->output('iso') }, $biblio->{'biblionumber'}, $itemnumber);
2344 # Charge a new rental fee, if applicable?
2345 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2346 if ( $charge > 0 ) {
2347 my $accountno = getnextacctno( $borrowernumber );
2348 my $item = GetBiblioFromItemNumber($itemnumber);
2349 my $manager_id = 0;
2350 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2351 $sth = $dbh->prepare(
2352 "INSERT INTO accountlines
2353 (date, borrowernumber, accountno, amount, manager_id,
2354 description,accounttype, amountoutstanding, itemnumber)
2355 VALUES (now(),?,?,?,?,?,?,?,?)"
2357 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2358 "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2359 'Rent', $charge, $itemnumber );
2360 $sth->finish;
2362 # Log the renewal
2363 UpdateStats( $branch, 'renew', $charge, '', $itemnumber, $item->{itype}, $borrowernumber);
2364 return $datedue;
2367 sub GetRenewCount {
2368 # check renewal status
2369 my ( $bornum, $itemno ) = @_;
2370 my $dbh = C4::Context->dbh;
2371 my $renewcount = 0;
2372 my $renewsallowed = 0;
2373 my $renewsleft = 0;
2375 my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2376 my $item = GetItem($itemno);
2378 # Look in the issues table for this item, lent to this borrower,
2379 # and not yet returned.
2381 # FIXME - I think this function could be redone to use only one SQL call.
2382 my $sth = $dbh->prepare(
2383 "select * from issues
2384 where (borrowernumber = ?)
2385 and (itemnumber = ?)"
2387 $sth->execute( $bornum, $itemno );
2388 my $data = $sth->fetchrow_hashref;
2389 $renewcount = $data->{'renewals'} if $data->{'renewals'};
2390 $sth->finish;
2391 # $item and $borrower should be calculated
2392 my $branchcode = _GetCircControlBranch($item, $borrower);
2394 my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2396 $renewsallowed = $issuingrule->{'renewalsallowed'};
2397 $renewsleft = $renewsallowed - $renewcount;
2398 if($renewsleft < 0){ $renewsleft = 0; }
2399 return ( $renewcount, $renewsallowed, $renewsleft );
2402 =head2 GetIssuingCharges
2404 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
2406 Calculate how much it would cost for a given patron to borrow a given
2407 item, including any applicable discounts.
2409 C<$itemnumber> is the item number of item the patron wishes to borrow.
2411 C<$borrowernumber> is the patron's borrower number.
2413 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
2414 and C<$item_type> is the code for the item's item type (e.g., C<VID>
2415 if it's a video).
2417 =cut
2419 sub GetIssuingCharges {
2421 # calculate charges due
2422 my ( $itemnumber, $borrowernumber ) = @_;
2423 my $charge = 0;
2424 my $dbh = C4::Context->dbh;
2425 my $item_type;
2427 # Get the book's item type and rental charge (via its biblioitem).
2428 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
2429 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
2430 $charge_query .= (C4::Context->preference('item-level_itypes'))
2431 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
2432 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
2434 $charge_query .= ' WHERE items.itemnumber =?';
2436 my $sth = $dbh->prepare($charge_query);
2437 $sth->execute($itemnumber);
2438 if ( my $item_data = $sth->fetchrow_hashref ) {
2439 $item_type = $item_data->{itemtype};
2440 $charge = $item_data->{rentalcharge};
2441 my $branch = C4::Branch::mybranch();
2442 my $discount_query = q|SELECT rentaldiscount,
2443 issuingrules.itemtype, issuingrules.branchcode
2444 FROM borrowers
2445 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
2446 WHERE borrowers.borrowernumber = ?
2447 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2448 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
2449 my $discount_sth = $dbh->prepare($discount_query);
2450 $discount_sth->execute( $borrowernumber, $item_type, $branch );
2451 my $discount_rules = $discount_sth->fetchall_arrayref({});
2452 if (@{$discount_rules}) {
2453 # We may have multiple rules so get the most specific
2454 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
2455 $charge = ( $charge * ( 100 - $discount ) ) / 100;
2459 $sth->finish; # we havent _explicitly_ fetched all rows
2460 return ( $charge, $item_type );
2463 # Select most appropriate discount rule from those returned
2464 sub _get_discount_from_rule {
2465 my ($rules_ref, $branch, $itemtype) = @_;
2466 my $discount;
2468 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
2469 $discount = $rules_ref->[0]->{rentaldiscount};
2470 return (defined $discount) ? $discount : 0;
2472 # could have up to 4 does one match $branch and $itemtype
2473 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
2474 if (@d) {
2475 $discount = $d[0]->{rentaldiscount};
2476 return (defined $discount) ? $discount : 0;
2478 # do we have item type + all branches
2479 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
2480 if (@d) {
2481 $discount = $d[0]->{rentaldiscount};
2482 return (defined $discount) ? $discount : 0;
2484 # do we all item types + this branch
2485 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
2486 if (@d) {
2487 $discount = $d[0]->{rentaldiscount};
2488 return (defined $discount) ? $discount : 0;
2490 # so all and all (surely we wont get here)
2491 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
2492 if (@d) {
2493 $discount = $d[0]->{rentaldiscount};
2494 return (defined $discount) ? $discount : 0;
2496 # none of the above
2497 return 0;
2500 =head2 AddIssuingCharge
2502 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
2504 =cut
2506 sub AddIssuingCharge {
2507 my ( $itemnumber, $borrowernumber, $charge ) = @_;
2508 my $dbh = C4::Context->dbh;
2509 my $nextaccntno = getnextacctno( $borrowernumber );
2510 my $manager_id = 0;
2511 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2512 my $query ="
2513 INSERT INTO accountlines
2514 (borrowernumber, itemnumber, accountno,
2515 date, amount, description, accounttype,
2516 amountoutstanding, manager_id)
2517 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
2519 my $sth = $dbh->prepare($query);
2520 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
2521 $sth->finish;
2524 =head2 GetTransfers
2526 GetTransfers($itemnumber);
2528 =cut
2530 sub GetTransfers {
2531 my ($itemnumber) = @_;
2533 my $dbh = C4::Context->dbh;
2535 my $query = '
2536 SELECT datesent,
2537 frombranch,
2538 tobranch
2539 FROM branchtransfers
2540 WHERE itemnumber = ?
2541 AND datearrived IS NULL
2543 my $sth = $dbh->prepare($query);
2544 $sth->execute($itemnumber);
2545 my @row = $sth->fetchrow_array();
2546 $sth->finish;
2547 return @row;
2550 =head2 GetTransfersFromTo
2552 @results = GetTransfersFromTo($frombranch,$tobranch);
2554 Returns the list of pending transfers between $from and $to branch
2556 =cut
2558 sub GetTransfersFromTo {
2559 my ( $frombranch, $tobranch ) = @_;
2560 return unless ( $frombranch && $tobranch );
2561 my $dbh = C4::Context->dbh;
2562 my $query = "
2563 SELECT itemnumber,datesent,frombranch
2564 FROM branchtransfers
2565 WHERE frombranch=?
2566 AND tobranch=?
2567 AND datearrived IS NULL
2569 my $sth = $dbh->prepare($query);
2570 $sth->execute( $frombranch, $tobranch );
2571 my @gettransfers;
2573 while ( my $data = $sth->fetchrow_hashref ) {
2574 push @gettransfers, $data;
2576 $sth->finish;
2577 return (@gettransfers);
2580 =head2 DeleteTransfer
2582 &DeleteTransfer($itemnumber);
2584 =cut
2586 sub DeleteTransfer {
2587 my ($itemnumber) = @_;
2588 my $dbh = C4::Context->dbh;
2589 my $sth = $dbh->prepare(
2590 "DELETE FROM branchtransfers
2591 WHERE itemnumber=?
2592 AND datearrived IS NULL "
2594 $sth->execute($itemnumber);
2595 $sth->finish;
2598 =head2 AnonymiseIssueHistory
2600 $rows = AnonymiseIssueHistory($date,$borrowernumber)
2602 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
2603 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
2605 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
2606 setting (force delete).
2608 return the number of affected rows.
2610 =cut
2612 sub AnonymiseIssueHistory {
2613 my $date = shift;
2614 my $borrowernumber = shift;
2615 my $dbh = C4::Context->dbh;
2616 my $query = "
2617 UPDATE old_issues
2618 SET borrowernumber = ?
2619 WHERE returndate < ?
2620 AND borrowernumber IS NOT NULL
2623 # The default of 0 does not work due to foreign key constraints
2624 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
2625 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
2626 my @bind_params = ($anonymouspatron, $date);
2627 if (defined $borrowernumber) {
2628 $query .= " AND borrowernumber = ?";
2629 push @bind_params, $borrowernumber;
2630 } else {
2631 $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
2633 my $sth = $dbh->prepare($query);
2634 $sth->execute(@bind_params);
2635 my $rows_affected = $sth->rows; ### doublecheck row count return function
2636 return $rows_affected;
2639 =head2 SendCirculationAlert
2641 Send out a C<check-in> or C<checkout> alert using the messaging system.
2643 B<Parameters>:
2645 =over 4
2647 =item type
2649 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
2651 =item item
2653 Hashref of information about the item being checked in or out.
2655 =item borrower
2657 Hashref of information about the borrower of the item.
2659 =item branch
2661 The branchcode from where the checkout or check-in took place.
2663 =back
2665 B<Example>:
2667 SendCirculationAlert({
2668 type => 'CHECKOUT',
2669 item => $item,
2670 borrower => $borrower,
2671 branch => $branch,
2674 =cut
2676 sub SendCirculationAlert {
2677 my ($opts) = @_;
2678 my ($type, $item, $borrower, $branch) =
2679 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
2680 my %message_name = (
2681 CHECKIN => 'Item_Check_in',
2682 CHECKOUT => 'Item_Checkout',
2684 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
2685 borrowernumber => $borrower->{borrowernumber},
2686 message_name => $message_name{$type},
2688 my $letter = C4::Letters::GetPreparedLetter (
2689 module => 'circulation',
2690 letter_code => $type,
2691 branchcode => $branch,
2692 tables => {
2693 'biblio' => $item->{biblionumber},
2694 'biblioitems' => $item->{biblionumber},
2695 'borrowers' => $borrower,
2696 'branches' => $branch,
2698 ) or return;
2700 my @transports = @{ $borrower_preferences->{transports} };
2701 # warn "no transports" unless @transports;
2702 for (@transports) {
2703 # warn "transport: $_";
2704 my $message = C4::Message->find_last_message($borrower, $type, $_);
2705 if (!$message) {
2706 #warn "create new message";
2707 C4::Message->enqueue($letter, $borrower, $_);
2708 } else {
2709 #warn "append to old message";
2710 $message->append($letter);
2711 $message->update;
2715 return $letter;
2718 =head2 updateWrongTransfer
2720 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
2722 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
2724 =cut
2726 sub updateWrongTransfer {
2727 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
2728 my $dbh = C4::Context->dbh;
2729 # first step validate the actual line of transfert .
2730 my $sth =
2731 $dbh->prepare(
2732 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
2734 $sth->execute($FromLibrary,$itemNumber);
2735 $sth->finish;
2737 # second step create a new line of branchtransfer to the right location .
2738 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
2740 #third step changing holdingbranch of item
2741 UpdateHoldingbranch($FromLibrary,$itemNumber);
2744 =head2 UpdateHoldingbranch
2746 $items = UpdateHoldingbranch($branch,$itmenumber);
2748 Simple methode for updating hodlingbranch in items BDD line
2750 =cut
2752 sub UpdateHoldingbranch {
2753 my ( $branch,$itemnumber ) = @_;
2754 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
2757 =head2 CalcDateDue
2759 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
2761 this function calculates the due date given the start date and configured circulation rules,
2762 checking against the holidays calendar as per the 'useDaysMode' syspref.
2763 C<$startdate> = C4::Dates object representing start date of loan period (assumed to be today)
2764 C<$itemtype> = itemtype code of item in question
2765 C<$branch> = location whose calendar to use
2766 C<$borrower> = Borrower object
2768 =cut
2770 sub CalcDateDue {
2771 my ($startdate,$itemtype,$branch,$borrower) = @_;
2772 my $datedue;
2773 my $loanlength = GetLoanLength($borrower->{'categorycode'},$itemtype, $branch);
2775 # if globalDueDate ON the datedue is set to that date
2776 if ( C4::Context->preference('globalDueDate')
2777 && ( C4::Context->preference('globalDueDate') =~ C4::Dates->regexp('syspref') ) ) {
2778 $datedue = C4::Dates->new( C4::Context->preference('globalDueDate') );
2779 } else {
2780 # otherwise, calculate the datedue as normal
2781 if(C4::Context->preference('useDaysMode') eq 'Days') { # ignoring calendar
2782 my $timedue = time + ($loanlength) * 86400;
2783 #FIXME - assumes now even though we take a startdate
2784 my @datearr = localtime($timedue);
2785 $datedue = C4::Dates->new( sprintf("%04d-%02d-%02d", 1900 + $datearr[5], $datearr[4] + 1, $datearr[3]), 'iso');
2786 } else {
2787 my $calendar = C4::Calendar->new( branchcode => $branch );
2788 $datedue = $calendar->addDate($startdate, $loanlength);
2792 # if Hard Due Dates are used, retreive them and apply as necessary
2793 my ($hardduedate, $hardduedatecompare) = GetHardDueDate($borrower->{'categorycode'},$itemtype, $branch);
2794 if ( $hardduedate && $hardduedate->output('iso') && $hardduedate->output('iso') ne '0000-00-00') {
2795 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
2796 if ( $datedue->output( 'iso' ) gt $hardduedate->output( 'iso' ) && $hardduedatecompare == -1) {
2797 $datedue = $hardduedate;
2798 # if the calculated date is before the 'after' Hard Due Date (floor), override
2799 } elsif ( $datedue->output( 'iso' ) lt $hardduedate->output( 'iso' ) && $hardduedatecompare == 1) {
2800 $datedue = $hardduedate;
2801 # if the hard due date is set to 'exactly', overrride
2802 } elsif ( $hardduedatecompare == 0) {
2803 $datedue = $hardduedate;
2805 # in all other cases, keep the date due as it is
2808 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
2809 if ( C4::Context->preference('ReturnBeforeExpiry') && $datedue->output('iso') gt $borrower->{dateexpiry} ) {
2810 $datedue = C4::Dates->new( $borrower->{dateexpiry}, 'iso' );
2813 return $datedue;
2816 =head2 CheckValidDatedue
2818 $newdatedue = CheckValidDatedue($date_due,$itemnumber,$branchcode);
2820 This function does not account for holiday exceptions nor does it handle the 'useDaysMode' syspref .
2821 To be replaced by CalcDateDue() once C4::Calendar use is tested.
2823 this function validates the loan length against the holidays calendar, and adjusts the due date as per the 'useDaysMode' syspref.
2824 C<$date_due> = returndate calculate with no day check
2825 C<$itemnumber> = itemnumber
2826 C<$branchcode> = location of issue (affected by 'CircControl' syspref)
2827 C<$loanlength> = loan length prior to adjustment
2829 =cut
2831 sub CheckValidDatedue {
2832 my ($date_due,$itemnumber,$branchcode)=@_;
2833 my @datedue=split('-',$date_due->output('iso'));
2834 my $years=$datedue[0];
2835 my $month=$datedue[1];
2836 my $day=$datedue[2];
2837 # die "Item# $itemnumber ($branchcode) due: " . ${date_due}->output() . "\n(Y,M,D) = ($years,$month,$day)":
2838 my $dow;
2839 for (my $i=0;$i<2;$i++){
2840 $dow=Day_of_Week($years,$month,$day);
2841 ($dow=0) if ($dow>6);
2842 my $result=CheckRepeatableHolidays($itemnumber,$dow,$branchcode);
2843 my $countspecial=CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
2844 my $countspecialrepeatable=CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
2845 if (($result ne '0') or ($countspecial ne '0') or ($countspecialrepeatable ne '0') ){
2846 $i=0;
2847 (($years,$month,$day) = Add_Delta_Days($years,$month,$day, 1))if ($i ne '1');
2850 my $newdatedue=C4::Dates->new(sprintf("%04d-%02d-%02d",$years,$month,$day),'iso');
2851 return $newdatedue;
2855 =head2 CheckRepeatableHolidays
2857 $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
2859 This function checks if the date due is a repeatable holiday
2861 C<$date_due> = returndate calculate with no day check
2862 C<$itemnumber> = itemnumber
2863 C<$branchcode> = localisation of issue
2865 =cut
2867 sub CheckRepeatableHolidays{
2868 my($itemnumber,$week_day,$branchcode)=@_;
2869 my $dbh = C4::Context->dbh;
2870 my $query = qq|SELECT count(*)
2871 FROM repeatable_holidays
2872 WHERE branchcode=?
2873 AND weekday=?|;
2874 my $sth = $dbh->prepare($query);
2875 $sth->execute($branchcode,$week_day);
2876 my $result=$sth->fetchrow;
2877 $sth->finish;
2878 return $result;
2882 =head2 CheckSpecialHolidays
2884 $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
2886 This function check if the date is a special holiday
2888 C<$years> = the years of datedue
2889 C<$month> = the month of datedue
2890 C<$day> = the day of datedue
2891 C<$itemnumber> = itemnumber
2892 C<$branchcode> = localisation of issue
2894 =cut
2896 sub CheckSpecialHolidays{
2897 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
2898 my $dbh = C4::Context->dbh;
2899 my $query=qq|SELECT count(*)
2900 FROM `special_holidays`
2901 WHERE year=?
2902 AND month=?
2903 AND day=?
2904 AND branchcode=?
2906 my $sth = $dbh->prepare($query);
2907 $sth->execute($years,$month,$day,$branchcode);
2908 my $countspecial=$sth->fetchrow ;
2909 $sth->finish;
2910 return $countspecial;
2913 =head2 CheckRepeatableSpecialHolidays
2915 $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
2917 This function check if the date is a repeatble special holidays
2919 C<$month> = the month of datedue
2920 C<$day> = the day of datedue
2921 C<$itemnumber> = itemnumber
2922 C<$branchcode> = localisation of issue
2924 =cut
2926 sub CheckRepeatableSpecialHolidays{
2927 my ($month,$day,$itemnumber,$branchcode) = @_;
2928 my $dbh = C4::Context->dbh;
2929 my $query=qq|SELECT count(*)
2930 FROM `repeatable_holidays`
2931 WHERE month=?
2932 AND day=?
2933 AND branchcode=?
2935 my $sth = $dbh->prepare($query);
2936 $sth->execute($month,$day,$branchcode);
2937 my $countspecial=$sth->fetchrow ;
2938 $sth->finish;
2939 return $countspecial;
2944 sub CheckValidBarcode{
2945 my ($barcode) = @_;
2946 my $dbh = C4::Context->dbh;
2947 my $query=qq|SELECT count(*)
2948 FROM items
2949 WHERE barcode=?
2951 my $sth = $dbh->prepare($query);
2952 $sth->execute($barcode);
2953 my $exist=$sth->fetchrow ;
2954 $sth->finish;
2955 return $exist;
2958 =head2 IsBranchTransferAllowed
2960 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
2962 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
2964 =cut
2966 sub IsBranchTransferAllowed {
2967 my ( $toBranch, $fromBranch, $code ) = @_;
2969 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
2971 my $limitType = C4::Context->preference("BranchTransferLimitsType");
2972 my $dbh = C4::Context->dbh;
2974 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
2975 $sth->execute( $toBranch, $fromBranch, $code );
2976 my $limit = $sth->fetchrow_hashref();
2978 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
2979 if ( $limit->{'limitId'} ) {
2980 return 0;
2981 } else {
2982 return 1;
2986 =head2 CreateBranchTransferLimit
2988 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
2990 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
2992 =cut
2994 sub CreateBranchTransferLimit {
2995 my ( $toBranch, $fromBranch, $code ) = @_;
2997 my $limitType = C4::Context->preference("BranchTransferLimitsType");
2999 my $dbh = C4::Context->dbh;
3001 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3002 $sth->execute( $code, $toBranch, $fromBranch );
3005 =head2 DeleteBranchTransferLimits
3007 DeleteBranchTransferLimits($frombranch);
3009 Deletes all the branch transfer limits for one branch
3011 =cut
3013 sub DeleteBranchTransferLimits {
3014 my $branch = shift;
3015 my $dbh = C4::Context->dbh;
3016 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3017 $sth->execute($branch);
3020 sub ReturnLostItem{
3021 my ( $borrowernumber, $itemnum ) = @_;
3023 MarkIssueReturned( $borrowernumber, $itemnum );
3024 my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3025 my @datearr = localtime(time);
3026 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3027 my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3028 ModItem({ paidfor => "Paid for by $bor $date" }, undef, $itemnum);
3032 sub LostItem{
3033 my ($itemnumber, $mark_returned, $charge_fee) = @_;
3035 my $dbh = C4::Context->dbh();
3036 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3037 FROM issues
3038 JOIN items USING (itemnumber)
3039 JOIN biblio USING (biblionumber)
3040 WHERE issues.itemnumber=?");
3041 $sth->execute($itemnumber);
3042 my $issues=$sth->fetchrow_hashref();
3043 $sth->finish;
3045 # if a borrower lost the item, add a replacement cost to the their record
3046 if ( my $borrowernumber = $issues->{borrowernumber} ){
3048 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}")
3049 if $charge_fee;
3050 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3051 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3052 MarkIssueReturned($borrowernumber,$itemnumber) if $mark_returned;
3056 sub GetOfflineOperations {
3057 my $dbh = C4::Context->dbh;
3058 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3059 $sth->execute(C4::Context->userenv->{'branch'});
3060 my $results = $sth->fetchall_arrayref({});
3061 $sth->finish;
3062 return $results;
3065 sub GetOfflineOperation {
3066 my $dbh = C4::Context->dbh;
3067 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3068 $sth->execute( shift );
3069 my $result = $sth->fetchrow_hashref;
3070 $sth->finish;
3071 return $result;
3074 sub AddOfflineOperation {
3075 my $dbh = C4::Context->dbh;
3076 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber) VALUES(?,?,?,?,?,?)");
3077 $sth->execute( @_ );
3078 return "Added.";
3081 sub DeleteOfflineOperation {
3082 my $dbh = C4::Context->dbh;
3083 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3084 $sth->execute( shift );
3085 return "Deleted.";
3088 sub ProcessOfflineOperation {
3089 my $operation = shift;
3091 my $report;
3092 if ( $operation->{action} eq 'return' ) {
3093 $report = ProcessOfflineReturn( $operation );
3094 } elsif ( $operation->{action} eq 'issue' ) {
3095 $report = ProcessOfflineIssue( $operation );
3098 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3100 return $report;
3103 sub ProcessOfflineReturn {
3104 my $operation = shift;
3106 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3108 if ( $itemnumber ) {
3109 my $issue = GetOpenIssue( $itemnumber );
3110 if ( $issue ) {
3111 MarkIssueReturned(
3112 $issue->{borrowernumber},
3113 $itemnumber,
3114 undef,
3115 $operation->{timestamp},
3117 ModItem(
3118 { renewals => 0, onloan => undef },
3119 $issue->{'biblionumber'},
3120 $itemnumber
3122 return "Success.";
3123 } else {
3124 return "Item not issued.";
3126 } else {
3127 return "Item not found.";
3131 sub ProcessOfflineIssue {
3132 my $operation = shift;
3134 my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3136 if ( $borrower->{borrowernumber} ) {
3137 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3138 unless ($itemnumber) {
3139 return "Barcode not found.";
3141 my $issue = GetOpenIssue( $itemnumber );
3143 if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3144 MarkIssueReturned(
3145 $issue->{borrowernumber},
3146 $itemnumber,
3147 undef,
3148 $operation->{timestamp},
3151 AddIssue(
3152 $borrower,
3153 $operation->{'barcode'},
3154 undef,
3156 $operation->{timestamp},
3157 undef,
3159 return "Success.";
3160 } else {
3161 return "Borrower not found.";
3167 =head2 TransferSlip
3169 TransferSlip($user_branch, $itemnumber, $to_branch)
3171 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3173 =cut
3175 sub TransferSlip {
3176 my ($branch, $itemnumber, $to_branch) = @_;
3178 my $item = GetItem( $itemnumber )
3179 or return;
3181 my $pulldate = C4::Dates->new();
3183 return C4::Letters::GetPreparedLetter (
3184 module => 'circulation',
3185 letter_code => 'TRANSFERSLIP',
3186 branchcode => $branch,
3187 tables => {
3188 'branches' => $to_branch,
3189 'biblio' => $item->{biblionumber},
3190 'items' => $item,
3198 __END__
3200 =head1 AUTHOR
3202 Koha Development Team <http://koha-community.org/>
3204 =cut