Bug 8597: Add CSS, JS, and images to ccsr theme
[koha.git] / C4 / Circulation.pm
blob499fd518a5c1669ba2031b835b6c6b61947ea00f
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 DateTime;
25 use C4::Context;
26 use C4::Stats;
27 use C4::Reserves;
28 use C4::Biblio;
29 use C4::Items;
30 use C4::Members;
31 use C4::Dates;
32 use C4::Dates qw(format_date);
33 use C4::Accounts;
34 use C4::ItemCirculationAlertPreference;
35 use C4::Message;
36 use C4::Debug;
37 use C4::Branch; # GetBranches
38 use C4::Log; # logaction
39 use C4::Koha qw(GetAuthorisedValueByCode);
40 use C4::Overdues qw(CalcFine UpdateFine);
41 use Algorithm::CheckDigits;
43 use Data::Dumper;
44 use Koha::DateUtils;
45 use Koha::Calendar;
46 use Carp;
47 use Date::Calc qw(
48 Today
49 Today_and_Now
50 Add_Delta_YM
51 Add_Delta_DHMS
52 Date_to_Days
53 Day_of_Week
54 Add_Delta_Days
56 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
58 BEGIN {
59 require Exporter;
60 $VERSION = 3.07.00.049; # for version checking
61 @ISA = qw(Exporter);
63 # FIXME subs that should probably be elsewhere
64 push @EXPORT, qw(
65 &barcodedecode
66 &LostItem
67 &ReturnLostItem
70 # subs to deal with issuing a book
71 push @EXPORT, qw(
72 &CanBookBeIssued
73 &CanBookBeRenewed
74 &AddIssue
75 &AddRenewal
76 &GetRenewCount
77 &GetItemIssue
78 &GetItemIssues
79 &GetIssuingCharges
80 &GetIssuingRule
81 &GetBranchBorrowerCircRule
82 &GetBranchItemRule
83 &GetBiblioIssues
84 &GetOpenIssue
85 &AnonymiseIssueHistory
88 # subs to deal with returns
89 push @EXPORT, qw(
90 &AddReturn
91 &MarkIssueReturned
94 # subs to deal with transfers
95 push @EXPORT, qw(
96 &transferbook
97 &GetTransfers
98 &GetTransfersFromTo
99 &updateWrongTransfer
100 &DeleteTransfer
101 &IsBranchTransferAllowed
102 &CreateBranchTransferLimit
103 &DeleteBranchTransferLimits
104 &TransferSlip
107 # subs to deal with offline circulation
108 push @EXPORT, qw(
109 &GetOfflineOperations
110 &GetOfflineOperation
111 &AddOfflineOperation
112 &DeleteOfflineOperation
113 &ProcessOfflineOperation
117 =head1 NAME
119 C4::Circulation - Koha circulation module
121 =head1 SYNOPSIS
123 use C4::Circulation;
125 =head1 DESCRIPTION
127 The functions in this module deal with circulation, issues, and
128 returns, as well as general information about the library.
129 Also deals with stocktaking.
131 =head1 FUNCTIONS
133 =head2 barcodedecode
135 $str = &barcodedecode($barcode, [$filter]);
137 Generic filter function for barcode string.
138 Called on every circ if the System Pref itemBarcodeInputFilter is set.
139 Will do some manipulation of the barcode for systems that deliver a barcode
140 to circulation.pl that differs from the barcode stored for the item.
141 For proper functioning of this filter, calling the function on the
142 correct barcode string (items.barcode) should return an unaltered barcode.
144 The optional $filter argument is to allow for testing or explicit
145 behavior that ignores the System Pref. Valid values are the same as the
146 System Pref options.
148 =cut
150 # FIXME -- the &decode fcn below should be wrapped into this one.
151 # FIXME -- these plugins should be moved out of Circulation.pm
153 sub barcodedecode {
154 my ($barcode, $filter) = @_;
155 my $branch = C4::Branch::mybranch();
156 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
157 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
158 if ($filter eq 'whitespace') {
159 $barcode =~ s/\s//g;
160 } elsif ($filter eq 'cuecat') {
161 chomp($barcode);
162 my @fields = split( /\./, $barcode );
163 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
164 ($#results == 2) and return $results[2];
165 } elsif ($filter eq 'T-prefix') {
166 if ($barcode =~ /^[Tt](\d)/) {
167 (defined($1) and $1 eq '0') and return $barcode;
168 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
170 return sprintf("T%07d", $barcode);
171 # FIXME: $barcode could be "T1", causing warning: substr outside of string
172 # Why drop the nonzero digit after the T?
173 # Why pass non-digits (or empty string) to "T%07d"?
174 } elsif ($filter eq 'libsuite8') {
175 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
176 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
177 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
178 }else{
179 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
182 } elsif ($filter eq 'EAN13') {
183 my $ean = CheckDigits('ean');
184 if ( $ean->is_valid($barcode) ) {
185 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
186 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
187 } else {
188 warn "# [$barcode] not valid EAN-13/UPC-A\n";
191 return $barcode; # return barcode, modified or not
194 =head2 decode
196 $str = &decode($chunk);
198 Decodes a segment of a string emitted by a CueCat barcode scanner and
199 returns it.
201 FIXME: Should be replaced with Barcode::Cuecat from CPAN
202 or Javascript based decoding on the client side.
204 =cut
206 sub decode {
207 my ($encoded) = @_;
208 my $seq =
209 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
210 my @s = map { index( $seq, $_ ); } split( //, $encoded );
211 my $l = ( $#s + 1 ) % 4;
212 if ($l) {
213 if ( $l == 1 ) {
214 # warn "Error: Cuecat decode parsing failed!";
215 return;
217 $l = 4 - $l;
218 $#s += $l;
220 my $r = '';
221 while ( $#s >= 0 ) {
222 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
223 $r .=
224 chr( ( $n >> 16 ) ^ 67 )
225 .chr( ( $n >> 8 & 255 ) ^ 67 )
226 .chr( ( $n & 255 ) ^ 67 );
227 @s = @s[ 4 .. $#s ];
229 $r = substr( $r, 0, length($r) - $l );
230 return $r;
233 =head2 transferbook
235 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
236 $barcode, $ignore_reserves);
238 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
240 C<$newbranch> is the code for the branch to which the item should be transferred.
242 C<$barcode> is the barcode of the item to be transferred.
244 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
245 Otherwise, if an item is reserved, the transfer fails.
247 Returns three values:
249 =over
251 =item $dotransfer
253 is true if the transfer was successful.
255 =item $messages
257 is a reference-to-hash which may have any of the following keys:
259 =over
261 =item C<BadBarcode>
263 There is no item in the catalog with the given barcode. The value is C<$barcode>.
265 =item C<IsPermanent>
267 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.
269 =item C<DestinationEqualsHolding>
271 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.
273 =item C<WasReturned>
275 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.
277 =item C<ResFound>
279 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>.
281 =item C<WasTransferred>
283 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
285 =back
287 =back
289 =cut
291 sub transferbook {
292 my ( $tbr, $barcode, $ignoreRs ) = @_;
293 my $messages;
294 my $dotransfer = 1;
295 my $branches = GetBranches();
296 my $itemnumber = GetItemnumberFromBarcode( $barcode );
297 my $issue = GetItemIssue($itemnumber);
298 my $biblio = GetBiblioFromItemNumber($itemnumber);
300 # bad barcode..
301 if ( not $itemnumber ) {
302 $messages->{'BadBarcode'} = $barcode;
303 $dotransfer = 0;
306 # get branches of book...
307 my $hbr = $biblio->{'homebranch'};
308 my $fbr = $biblio->{'holdingbranch'};
310 # if using Branch Transfer Limits
311 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
312 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
313 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
314 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
315 $dotransfer = 0;
317 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
318 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
319 $dotransfer = 0;
323 # if is permanent...
324 if ( $hbr && $branches->{$hbr}->{'PE'} ) {
325 $messages->{'IsPermanent'} = $hbr;
326 $dotransfer = 0;
329 # can't transfer book if is already there....
330 if ( $fbr eq $tbr ) {
331 $messages->{'DestinationEqualsHolding'} = 1;
332 $dotransfer = 0;
335 # check if it is still issued to someone, return it...
336 if ($issue->{borrowernumber}) {
337 AddReturn( $barcode, $fbr );
338 $messages->{'WasReturned'} = $issue->{borrowernumber};
341 # find reserves.....
342 # That'll save a database query.
343 my ( $resfound, $resrec, undef ) =
344 CheckReserves( $itemnumber );
345 if ( $resfound and not $ignoreRs ) {
346 $resrec->{'ResFound'} = $resfound;
348 # $messages->{'ResFound'} = $resrec;
349 $dotransfer = 1;
352 #actually do the transfer....
353 if ($dotransfer) {
354 ModItemTransfer( $itemnumber, $fbr, $tbr );
356 # don't need to update MARC anymore, we do it in batch now
357 $messages->{'WasTransfered'} = 1;
360 ModDateLastSeen( $itemnumber );
361 return ( $dotransfer, $messages, $biblio );
365 sub TooMany {
366 my $borrower = shift;
367 my $biblionumber = shift;
368 my $item = shift;
369 my $cat_borrower = $borrower->{'categorycode'};
370 my $dbh = C4::Context->dbh;
371 my $branch;
372 # Get which branchcode we need
373 $branch = _GetCircControlBranch($item,$borrower);
374 my $type = (C4::Context->preference('item-level_itypes'))
375 ? $item->{'itype'} # item-level
376 : $item->{'itemtype'}; # biblio-level
378 # given branch, patron category, and item type, determine
379 # applicable issuing rule
380 my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
382 # if a rule is found and has a loan limit set, count
383 # how many loans the patron already has that meet that
384 # rule
385 if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
386 my @bind_params;
387 my $count_query = "SELECT COUNT(*) FROM issues
388 JOIN items USING (itemnumber) ";
390 my $rule_itemtype = $issuing_rule->{itemtype};
391 if ($rule_itemtype eq "*") {
392 # matching rule has the default item type, so count only
393 # those existing loans that don't fall under a more
394 # specific rule
395 if (C4::Context->preference('item-level_itypes')) {
396 $count_query .= " WHERE items.itype NOT IN (
397 SELECT itemtype FROM issuingrules
398 WHERE branchcode = ?
399 AND (categorycode = ? OR categorycode = ?)
400 AND itemtype <> '*'
401 ) ";
402 } else {
403 $count_query .= " JOIN biblioitems USING (biblionumber)
404 WHERE biblioitems.itemtype NOT IN (
405 SELECT itemtype FROM issuingrules
406 WHERE branchcode = ?
407 AND (categorycode = ? OR categorycode = ?)
408 AND itemtype <> '*'
409 ) ";
411 push @bind_params, $issuing_rule->{branchcode};
412 push @bind_params, $issuing_rule->{categorycode};
413 push @bind_params, $cat_borrower;
414 } else {
415 # rule has specific item type, so count loans of that
416 # specific item type
417 if (C4::Context->preference('item-level_itypes')) {
418 $count_query .= " WHERE items.itype = ? ";
419 } else {
420 $count_query .= " JOIN biblioitems USING (biblionumber)
421 WHERE biblioitems.itemtype= ? ";
423 push @bind_params, $type;
426 $count_query .= " AND borrowernumber = ? ";
427 push @bind_params, $borrower->{'borrowernumber'};
428 my $rule_branch = $issuing_rule->{branchcode};
429 if ($rule_branch ne "*") {
430 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
431 $count_query .= " AND issues.branchcode = ? ";
432 push @bind_params, $branch;
433 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
434 ; # if branch is the patron's home branch, then count all loans by patron
435 } else {
436 $count_query .= " AND items.homebranch = ? ";
437 push @bind_params, $branch;
441 my $count_sth = $dbh->prepare($count_query);
442 $count_sth->execute(@bind_params);
443 my ($current_loan_count) = $count_sth->fetchrow_array;
445 my $max_loans_allowed = $issuing_rule->{'maxissueqty'};
446 if ($current_loan_count >= $max_loans_allowed) {
447 return ($current_loan_count, $max_loans_allowed);
451 # Now count total loans against the limit for the branch
452 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
453 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
454 my @bind_params = ();
455 my $branch_count_query = "SELECT COUNT(*) FROM issues
456 JOIN items USING (itemnumber)
457 WHERE borrowernumber = ? ";
458 push @bind_params, $borrower->{borrowernumber};
460 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
461 $branch_count_query .= " AND issues.branchcode = ? ";
462 push @bind_params, $branch;
463 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
464 ; # if branch is the patron's home branch, then count all loans by patron
465 } else {
466 $branch_count_query .= " AND items.homebranch = ? ";
467 push @bind_params, $branch;
469 my $branch_count_sth = $dbh->prepare($branch_count_query);
470 $branch_count_sth->execute(@bind_params);
471 my ($current_loan_count) = $branch_count_sth->fetchrow_array;
473 my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty};
474 if ($current_loan_count >= $max_loans_allowed) {
475 return ($current_loan_count, $max_loans_allowed);
479 # OK, the patron can issue !!!
480 return;
483 =head2 itemissues
485 @issues = &itemissues($biblioitemnumber, $biblio);
487 Looks up information about who has borrowed the bookZ<>(s) with the
488 given biblioitemnumber.
490 C<$biblio> is ignored.
492 C<&itemissues> returns an array of references-to-hash. The keys
493 include the fields from the C<items> table in the Koha database.
494 Additional keys include:
496 =over 4
498 =item C<date_due>
500 If the item is currently on loan, this gives the due date.
502 If the item is not on loan, then this is either "Available" or
503 "Cancelled", if the item has been withdrawn.
505 =item C<card>
507 If the item is currently on loan, this gives the card number of the
508 patron who currently has the item.
510 =item C<timestamp0>, C<timestamp1>, C<timestamp2>
512 These give the timestamp for the last three times the item was
513 borrowed.
515 =item C<card0>, C<card1>, C<card2>
517 The card number of the last three patrons who borrowed this item.
519 =item C<borrower0>, C<borrower1>, C<borrower2>
521 The borrower number of the last three patrons who borrowed this item.
523 =back
525 =cut
528 sub itemissues {
529 my ( $bibitem, $biblio ) = @_;
530 my $dbh = C4::Context->dbh;
531 my $sth =
532 $dbh->prepare("Select * from items where items.biblioitemnumber = ?")
533 || die $dbh->errstr;
534 my $i = 0;
535 my @results;
537 $sth->execute($bibitem) || die $sth->errstr;
539 while ( my $data = $sth->fetchrow_hashref ) {
541 # Find out who currently has this item.
542 # FIXME - Wouldn't it be better to do this as a left join of
543 # some sort? Currently, this code assumes that if
544 # fetchrow_hashref() fails, then the book is on the shelf.
545 # fetchrow_hashref() can fail for any number of reasons (e.g.,
546 # database server crash), not just because no items match the
547 # search criteria.
548 my $sth2 = $dbh->prepare(
549 "SELECT * FROM issues
550 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
551 WHERE itemnumber = ?
555 $sth2->execute( $data->{'itemnumber'} );
556 if ( my $data2 = $sth2->fetchrow_hashref ) {
557 $data->{'date_due'} = $data2->{'date_due'};
558 $data->{'card'} = $data2->{'cardnumber'};
559 $data->{'borrower'} = $data2->{'borrowernumber'};
561 else {
562 $data->{'date_due'} = ($data->{'wthdrawn'} eq '1') ? 'Cancelled' : 'Available';
566 # Find the last 3 people who borrowed this item.
567 $sth2 = $dbh->prepare(
568 "SELECT * FROM old_issues
569 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
570 WHERE itemnumber = ?
571 ORDER BY returndate DESC,timestamp DESC"
574 $sth2->execute( $data->{'itemnumber'} );
575 for ( my $i2 = 0 ; $i2 < 2 ; $i2++ )
576 { # FIXME : error if there is less than 3 pple borrowing this item
577 if ( my $data2 = $sth2->fetchrow_hashref ) {
578 $data->{"timestamp$i2"} = $data2->{'timestamp'};
579 $data->{"card$i2"} = $data2->{'cardnumber'};
580 $data->{"borrower$i2"} = $data2->{'borrowernumber'};
581 } # if
582 } # for
584 $results[$i] = $data;
585 $i++;
588 return (@results);
591 =head2 CanBookBeIssued
593 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
594 $barcode, $duedatespec, $inprocess, $ignore_reserves );
596 Check if a book can be issued.
598 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
600 =over 4
602 =item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
604 =item C<$barcode> is the bar code of the book being issued.
606 =item C<$duedatespec> is a C4::Dates object.
608 =item C<$inprocess> boolean switch
609 =item C<$ignore_reserves> boolean switch
611 =back
613 Returns :
615 =over 4
617 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
618 Possible values are :
620 =back
622 =head3 INVALID_DATE
624 sticky due date is invalid
626 =head3 GNA
628 borrower gone with no address
630 =head3 CARD_LOST
632 borrower declared it's card lost
634 =head3 DEBARRED
636 borrower debarred
638 =head3 UNKNOWN_BARCODE
640 barcode unknown
642 =head3 NOT_FOR_LOAN
644 item is not for loan
646 =head3 WTHDRAWN
648 item withdrawn.
650 =head3 RESTRICTED
652 item is restricted (set by ??)
654 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
655 could be prevented, but ones that can be overriden by the operator.
657 Possible values are :
659 =head3 DEBT
661 borrower has debts.
663 =head3 RENEW_ISSUE
665 renewing, not issuing
667 =head3 ISSUED_TO_ANOTHER
669 issued to someone else.
671 =head3 RESERVED
673 reserved for someone else.
675 =head3 INVALID_DATE
677 sticky due date is invalid or due date in the past
679 =head3 TOO_MANY
681 if the borrower borrows to much things
683 =cut
685 sub CanBookBeIssued {
686 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves ) = @_;
687 my %needsconfirmation; # filled with problems that needs confirmations
688 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
689 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
691 my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
692 my $issue = GetItemIssue($item->{itemnumber});
693 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
694 $item->{'itemtype'}=$item->{'itype'};
695 my $dbh = C4::Context->dbh;
697 # MANDATORY CHECKS - unless item exists, nothing else matters
698 unless ( $item->{barcode} ) {
699 $issuingimpossible{UNKNOWN_BARCODE} = 1;
701 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
704 # DUE DATE is OK ? -- should already have checked.
706 if ($duedate && ref $duedate ne 'DateTime') {
707 $duedate = dt_from_string($duedate);
709 my $now = DateTime->now( time_zone => C4::Context->tz() );
710 unless ( $duedate ) {
711 my $issuedate = $now->clone();
713 my $branch = _GetCircControlBranch($item,$borrower);
714 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
715 $duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
717 # Offline circ calls AddIssue directly, doesn't run through here
718 # So issuingimpossible should be ok.
720 if ($duedate) {
721 my $today = $now->clone();
722 $today->truncate( to => 'minute');
723 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
724 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
726 } else {
727 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
731 # BORROWER STATUS
733 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
734 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
735 &UpdateStats(C4::Context->userenv->{'branch'},'localuse','','',$item->{'itemnumber'},$item->{'itemtype'},$borrower->{'borrowernumber'}, undef, $item->{'ccode'});
736 ModDateLastSeen( $item->{'itemnumber'} );
737 return( { STATS => 1 }, {});
739 if ( $borrower->{flags}->{GNA} ) {
740 $issuingimpossible{GNA} = 1;
742 if ( $borrower->{flags}->{'LOST'} ) {
743 $issuingimpossible{CARD_LOST} = 1;
745 if ( $borrower->{flags}->{'DBARRED'} ) {
746 $issuingimpossible{DEBARRED} = 1;
748 if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
749 $issuingimpossible{EXPIRED} = 1;
750 } else {
751 my ($y, $m, $d) = split /-/,$borrower->{'dateexpiry'};
752 if ($y && $m && $d) { # are we really writing oinvalid dates to borrs
753 my $expiry_dt = DateTime->new(
754 year => $y,
755 month => $m,
756 day => $d,
757 time_zone => C4::Context->tz,
759 $expiry_dt->truncate( to => 'day');
760 my $today = $now->clone()->truncate(to => 'day');
761 if (DateTime->compare($today, $expiry_dt) == 1) {
762 $issuingimpossible{EXPIRED} = 1;
764 } else {
765 carp("Invalid expity date in borr");
766 $issuingimpossible{EXPIRED} = 1;
770 # BORROWER STATUS
773 # DEBTS
774 my ($amount) =
775 C4::Members::GetMemberAccountRecords( $borrower->{'borrowernumber'}, '' && $duedate->ymd() );
776 my $amountlimit = C4::Context->preference("noissuescharge");
777 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
778 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
779 if ( C4::Context->preference("IssuingInProcess") ) {
780 if ( $amount > $amountlimit && !$inprocess && !$allowfineoverride) {
781 $issuingimpossible{DEBT} = sprintf( "%.2f", $amount );
782 } elsif ( $amount > $amountlimit && !$inprocess && $allowfineoverride) {
783 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
784 } elsif ( $allfinesneedoverride && $amount > 0 && $amount <= $amountlimit && !$inprocess ) {
785 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
788 else {
789 if ( $amount > $amountlimit && $allowfineoverride ) {
790 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
791 } elsif ( $amount > $amountlimit && !$allowfineoverride) {
792 $issuingimpossible{DEBT} = sprintf( "%.2f", $amount );
793 } elsif ( $amount > 0 && $allfinesneedoverride ) {
794 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
798 my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'});
799 if ($blocktype == -1) {
800 ## patron has outstanding overdue loans
801 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
802 $issuingimpossible{USERBLOCKEDOVERDUE} = $count;
804 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
805 $needsconfirmation{USERBLOCKEDOVERDUE} = $count;
807 } elsif($blocktype == 1) {
808 # patron has accrued fine days
809 $issuingimpossible{USERBLOCKEDREMAINING} = $count;
813 # JB34 CHECKS IF BORROWERS DONT HAVE ISSUE TOO MANY BOOKS
815 my ($current_loan_count, $max_loans_allowed) = TooMany( $borrower, $item->{biblionumber}, $item );
816 # if TooMany max_loans_allowed returns 0 the user doesn't have permission to check out this book
817 if (defined $max_loans_allowed && $max_loans_allowed == 0) {
818 $needsconfirmation{PATRON_CANT} = 1;
819 } else {
820 if($max_loans_allowed){
821 $needsconfirmation{TOO_MANY} = 1;
822 $needsconfirmation{current_loan_count} = $current_loan_count;
823 $needsconfirmation{max_loans_allowed} = $max_loans_allowed;
828 # ITEM CHECKING
830 if ( $item->{'notforloan'}
831 && $item->{'notforloan'} > 0 )
833 if(!C4::Context->preference("AllowNotForLoanOverride")){
834 $issuingimpossible{NOT_FOR_LOAN} = 1;
835 }else{
836 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
839 elsif ( !$item->{'notforloan'} ){
840 # we have to check itemtypes.notforloan also
841 if (C4::Context->preference('item-level_itypes')){
842 # this should probably be a subroutine
843 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
844 $sth->execute($item->{'itemtype'});
845 my $notforloan=$sth->fetchrow_hashref();
846 $sth->finish();
847 if ($notforloan->{'notforloan'}) {
848 if (!C4::Context->preference("AllowNotForLoanOverride")) {
849 $issuingimpossible{NOT_FOR_LOAN} = 1;
850 } else {
851 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
855 elsif ($biblioitem->{'notforloan'} == 1){
856 if (!C4::Context->preference("AllowNotForLoanOverride")) {
857 $issuingimpossible{NOT_FOR_LOAN} = 1;
858 } else {
859 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
863 if ( $item->{'wthdrawn'} && $item->{'wthdrawn'} > 0 )
865 $issuingimpossible{WTHDRAWN} = 1;
867 if ( $item->{'restricted'}
868 && $item->{'restricted'} == 1 )
870 $issuingimpossible{RESTRICTED} = 1;
872 if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
873 my $code = GetAuthorisedValueByCode( 'LOST', $item->{'itemlost'} );
874 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
875 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
877 if ( C4::Context->preference("IndependantBranches") ) {
878 my $userenv = C4::Context->userenv;
879 if ( ($userenv) && ( $userenv->{flags} % 2 != 1 ) ) {
880 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1
881 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} );
882 $needsconfirmation{BORRNOTSAMEBRANCH} = GetBranchName( $borrower->{'branchcode'} )
883 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
888 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
890 if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} )
893 # Already issued to current borrower. Ask whether the loan should
894 # be renewed.
895 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
896 $borrower->{'borrowernumber'},
897 $item->{'itemnumber'}
899 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
900 $issuingimpossible{NO_MORE_RENEWALS} = 1;
902 else {
903 $needsconfirmation{RENEW_ISSUE} = 1;
906 elsif ($issue->{borrowernumber}) {
908 # issued to someone else
909 my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
911 # warn "=>.$currborinfo->{'firstname'} $currborinfo->{'surname'} ($currborinfo->{'cardnumber'})";
912 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
913 $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
914 $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
915 $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
916 $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
919 unless ( $ignore_reserves ) {
920 # See if the item is on reserve.
921 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
922 if ($restype) {
923 my $resbor = $res->{'borrowernumber'};
924 if ( $resbor ne $borrower->{'borrowernumber'} ) {
925 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
926 my $branchname = GetBranchName( $res->{'branchcode'} );
927 if ( $restype eq "Waiting" )
929 # The item is on reserve and waiting, but has been
930 # reserved by some other patron.
931 $needsconfirmation{RESERVE_WAITING} = 1;
932 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
933 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
934 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
935 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
936 $needsconfirmation{'resbranchname'} = $branchname;
937 $needsconfirmation{'reswaitingdate'} = format_date($res->{'waitingdate'});
939 elsif ( $restype eq "Reserved" ) {
940 # The item is on reserve for someone else.
941 $needsconfirmation{RESERVED} = 1;
942 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
943 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
944 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
945 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
946 $needsconfirmation{'resbranchname'} = $branchname;
947 $needsconfirmation{'resreservedate'} = format_date($res->{'reservedate'});
953 # CHECK AGE RESTRICTION
956 # get $marker from preferences. Could be something like "FSK|PEGI|Alter|Age:"
957 my $markers = C4::Context->preference('AgeRestrictionMarker' );
958 my $bibvalues = $biblioitem->{'agerestriction'};
959 if (($markers)&&($bibvalues))
961 # Split $bibvalues to something like FSK 16 or PEGI 6
962 my @values = split ' ', $bibvalues;
964 # Search first occurence of one of the markers
965 my @markers = split /\|/, $markers;
966 my $index = 0;
967 my $take = -1;
968 for my $value (@values) {
969 $index ++;
970 for my $marker (@markers) {
971 $marker =~ s/^\s+//; #remove leading spaces
972 $marker =~ s/\s+$//; #remove trailing spaces
973 if (uc($marker) eq uc($value)) {
974 $take = $index;
975 last;
978 if ($take > -1) {
979 last;
982 # Index points to the next value
983 my $restrictionyear = 0;
984 if (($take <= $#values) && ($take >= 0)){
985 $restrictionyear += $values[$take];
988 if ($restrictionyear > 0) {
989 if ( $borrower->{'dateofbirth'} ) {
990 my @alloweddate = split /-/,$borrower->{'dateofbirth'} ;
991 $alloweddate[0] += $restrictionyear;
992 #Prevent runime eror on leap year (invalid date)
993 if (($alloweddate[1] == 2) && ($alloweddate[2] == 29)) {
994 $alloweddate[2] = 28;
997 if ( Date_to_Days(Today) < Date_to_Days(@alloweddate) -1 ) {
998 if (C4::Context->preference('AgeRestrictionOverride' )) {
999 $needsconfirmation{AGE_RESTRICTION} = "$bibvalues";
1001 else {
1002 $issuingimpossible{AGE_RESTRICTION} = "$bibvalues";
1008 return ( \%issuingimpossible, \%needsconfirmation, \%alerts );
1011 =head2 CanBookBeReturned
1013 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1015 Check whether the item can be returned to the provided branch
1017 =over 4
1019 =item C<$item> is a hash of item information as returned from GetItem
1021 =item C<$branch> is the branchcode where the return is taking place
1023 =back
1025 Returns:
1027 =over 4
1029 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1031 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1033 =cut
1035 sub CanBookBeReturned {
1036 my ($item, $branch) = @_;
1037 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1039 # assume return is allowed to start
1040 my $allowed = 1;
1041 my $message;
1043 # identify all cases where return is forbidden
1044 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1045 $allowed = 0;
1046 $message = $item->{'homebranch'};
1047 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1048 $allowed = 0;
1049 $message = $item->{'holdingbranch'};
1050 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1051 $allowed = 0;
1052 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1055 return ($allowed, $message);
1058 =head2 AddIssue
1060 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1062 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1064 =over 4
1066 =item C<$borrower> is a hash with borrower informations (from GetMember or GetMemberDetails).
1068 =item C<$barcode> is the barcode of the item being issued.
1070 =item C<$datedue> is a C4::Dates object for the max date of return, i.e. the date due (optional).
1071 Calculated if empty.
1073 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1075 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1076 Defaults to today. Unlike C<$datedue>, NOT a C4::Dates object, unfortunately.
1078 AddIssue does the following things :
1080 - step 01: check that there is a borrowernumber & a barcode provided
1081 - check for RENEWAL (book issued & being issued to the same patron)
1082 - renewal YES = Calculate Charge & renew
1083 - renewal NO =
1084 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1085 * RESERVE PLACED ?
1086 - fill reserve if reserve to this patron
1087 - cancel reserve or not, otherwise
1088 * TRANSFERT PENDING ?
1089 - complete the transfert
1090 * ISSUE THE BOOK
1092 =back
1094 =cut
1096 sub AddIssue {
1097 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode) = @_;
1098 my $dbh = C4::Context->dbh;
1099 my $barcodecheck=CheckValidBarcode($barcode);
1100 if ($datedue && ref $datedue ne 'DateTime') {
1101 $datedue = dt_from_string($datedue);
1103 # $issuedate defaults to today.
1104 if ( ! defined $issuedate ) {
1105 $issuedate = DateTime->now(time_zone => C4::Context->tz());
1107 else {
1108 if ( ref $issuedate ne 'DateTime') {
1109 $issuedate = dt_from_string($issuedate);
1113 if ($borrower and $barcode and $barcodecheck ne '0'){#??? wtf
1114 # find which item we issue
1115 my $item = GetItem('', $barcode) or return undef; # if we don't get an Item, abort.
1116 my $branch = _GetCircControlBranch($item,$borrower);
1118 # get actual issuing if there is one
1119 my $actualissue = GetItemIssue( $item->{itemnumber});
1121 # get biblioinformation for this item
1122 my $biblio = GetBiblioFromItemNumber($item->{itemnumber});
1125 # check if we just renew the issue.
1127 if ($actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}) {
1128 $datedue = AddRenewal(
1129 $borrower->{'borrowernumber'},
1130 $item->{'itemnumber'},
1131 $branch,
1132 $datedue,
1133 $issuedate, # here interpreted as the renewal date
1136 else {
1137 # it's NOT a renewal
1138 if ( $actualissue->{borrowernumber}) {
1139 # This book is currently on loan, but not to the person
1140 # who wants to borrow it now. mark it returned before issuing to the new borrower
1141 AddReturn(
1142 $item->{'barcode'},
1143 C4::Context->userenv->{'branch'}
1147 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1148 # Starting process for transfer job (checking transfert and validate it if we have one)
1149 my ($datesent) = GetTransfers($item->{'itemnumber'});
1150 if ($datesent) {
1151 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1152 my $sth =
1153 $dbh->prepare(
1154 "UPDATE branchtransfers
1155 SET datearrived = now(),
1156 tobranch = ?,
1157 comments = 'Forced branchtransfer'
1158 WHERE itemnumber= ? AND datearrived IS NULL"
1160 $sth->execute(C4::Context->userenv->{'branch'},$item->{'itemnumber'});
1163 # Record in the database the fact that the book was issued.
1164 my $sth =
1165 $dbh->prepare(
1166 "INSERT INTO issues
1167 (borrowernumber, itemnumber,issuedate, date_due, branchcode)
1168 VALUES (?,?,?,?,?)"
1170 unless ($datedue) {
1171 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'};
1172 $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1175 $datedue->truncate( to => 'minute');
1176 $sth->execute(
1177 $borrower->{'borrowernumber'}, # borrowernumber
1178 $item->{'itemnumber'}, # itemnumber
1179 $issuedate->strftime('%Y-%m-%d %H:%M:00'), # issuedate
1180 $datedue->strftime('%Y-%m-%d %H:%M:00'), # date_due
1181 C4::Context->userenv->{'branch'} # branchcode
1183 if ( C4::Context->preference('ReturnToShelvingCart') ) { ## ReturnToShelvingCart is on, anything issued should be taken off the cart.
1184 CartToShelf( $item->{'itemnumber'} );
1186 $item->{'issues'}++;
1187 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1188 UpdateTotalIssues($item->{'biblionumber'}, 1);
1191 ## If item was lost, it has now been found, reverse any list item charges if neccessary.
1192 if ( $item->{'itemlost'} ) {
1193 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, $item->{'barcode'} );
1196 ModItem({ issues => $item->{'issues'},
1197 holdingbranch => C4::Context->userenv->{'branch'},
1198 itemlost => 0,
1199 datelastborrowed => DateTime->now(time_zone => C4::Context->tz())->ymd(),
1200 onloan => $datedue->ymd(),
1201 }, $item->{'biblionumber'}, $item->{'itemnumber'});
1202 ModDateLastSeen( $item->{'itemnumber'} );
1204 # If it costs to borrow this book, charge it to the patron's account.
1205 my ( $charge, $itemtype ) = GetIssuingCharges(
1206 $item->{'itemnumber'},
1207 $borrower->{'borrowernumber'}
1209 if ( $charge > 0 ) {
1210 AddIssuingCharge(
1211 $item->{'itemnumber'},
1212 $borrower->{'borrowernumber'}, $charge
1214 $item->{'charge'} = $charge;
1217 # Record the fact that this book was issued.
1218 &UpdateStats(
1219 C4::Context->userenv->{'branch'},
1220 'issue', $charge,
1221 ($sipmode ? "SIP-$sipmode" : ''), $item->{'itemnumber'},
1222 $item->{'itype'}, $borrower->{'borrowernumber'}, undef, $item->{'ccode'}
1225 # Send a checkout slip.
1226 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1227 my %conditions = (
1228 branchcode => $branch,
1229 categorycode => $borrower->{categorycode},
1230 item_type => $item->{itype},
1231 notification => 'CHECKOUT',
1233 if ($circulation_alert->is_enabled_for(\%conditions)) {
1234 SendCirculationAlert({
1235 type => 'CHECKOUT',
1236 item => $item,
1237 borrower => $borrower,
1238 branch => $branch,
1243 logaction("CIRCULATION", "ISSUE", $borrower->{'borrowernumber'}, $biblio->{'biblionumber'})
1244 if C4::Context->preference("IssueLog");
1246 return ($datedue); # not necessarily the same as when it came in!
1249 =head2 GetLoanLength
1251 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1253 Get loan length for an itemtype, a borrower type and a branch
1255 =cut
1257 sub GetLoanLength {
1258 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1259 my $dbh = C4::Context->dbh;
1260 my $sth =
1261 $dbh->prepare(
1262 'select issuelength, lengthunit from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null'
1264 # warn "in get loan lenght $borrowertype $itemtype $branchcode ";
1265 # try to find issuelength & return the 1st available.
1266 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1267 $sth->execute( $borrowertype, $itemtype, $branchcode );
1268 my $loanlength = $sth->fetchrow_hashref;
1269 return $loanlength
1270 if defined($loanlength) && $loanlength->{issuelength};
1272 $sth->execute( $borrowertype, '*', $branchcode );
1273 $loanlength = $sth->fetchrow_hashref;
1274 return $loanlength
1275 if defined($loanlength) && $loanlength->{issuelength};
1277 $sth->execute( '*', $itemtype, $branchcode );
1278 $loanlength = $sth->fetchrow_hashref;
1279 return $loanlength
1280 if defined($loanlength) && $loanlength->{issuelength};
1282 $sth->execute( '*', '*', $branchcode );
1283 $loanlength = $sth->fetchrow_hashref;
1284 return $loanlength
1285 if defined($loanlength) && $loanlength->{issuelength};
1287 $sth->execute( $borrowertype, $itemtype, '*' );
1288 $loanlength = $sth->fetchrow_hashref;
1289 return $loanlength
1290 if defined($loanlength) && $loanlength->{issuelength};
1292 $sth->execute( $borrowertype, '*', '*' );
1293 $loanlength = $sth->fetchrow_hashref;
1294 return $loanlength
1295 if defined($loanlength) && $loanlength->{issuelength};
1297 $sth->execute( '*', $itemtype, '*' );
1298 $loanlength = $sth->fetchrow_hashref;
1299 return $loanlength
1300 if defined($loanlength) && $loanlength->{issuelength};
1302 $sth->execute( '*', '*', '*' );
1303 $loanlength = $sth->fetchrow_hashref;
1304 return $loanlength
1305 if defined($loanlength) && $loanlength->{issuelength};
1307 # if no rule is set => 21 days (hardcoded)
1308 return {
1309 issuelength => 21,
1310 lengthunit => 'days',
1316 =head2 GetHardDueDate
1318 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1320 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1322 =cut
1324 sub GetHardDueDate {
1325 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1327 my $rule = GetIssuingRule( $borrowertype, $itemtype, $branchcode );
1329 if ( defined( $rule ) ) {
1330 if ( $rule->{hardduedate} ) {
1331 return (dt_from_string($rule->{hardduedate}, 'iso'),$rule->{hardduedatecompare});
1332 } else {
1333 return (undef, undef);
1338 =head2 GetIssuingRule
1340 my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
1342 FIXME - This is a copy-paste of GetLoanLength
1343 as a stop-gap. Do not wish to change API for GetLoanLength
1344 this close to release, however, Overdues::GetIssuingRules is broken.
1346 Get the issuing rule for an itemtype, a borrower type and a branch
1347 Returns a hashref from the issuingrules table.
1349 =cut
1351 sub GetIssuingRule {
1352 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1353 my $dbh = C4::Context->dbh;
1354 my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null" );
1355 my $irule;
1357 $sth->execute( $borrowertype, $itemtype, $branchcode );
1358 $irule = $sth->fetchrow_hashref;
1359 return $irule if defined($irule) ;
1361 $sth->execute( $borrowertype, "*", $branchcode );
1362 $irule = $sth->fetchrow_hashref;
1363 return $irule if defined($irule) ;
1365 $sth->execute( "*", $itemtype, $branchcode );
1366 $irule = $sth->fetchrow_hashref;
1367 return $irule if defined($irule) ;
1369 $sth->execute( "*", "*", $branchcode );
1370 $irule = $sth->fetchrow_hashref;
1371 return $irule if defined($irule) ;
1373 $sth->execute( $borrowertype, $itemtype, "*" );
1374 $irule = $sth->fetchrow_hashref;
1375 return $irule if defined($irule) ;
1377 $sth->execute( $borrowertype, "*", "*" );
1378 $irule = $sth->fetchrow_hashref;
1379 return $irule if defined($irule) ;
1381 $sth->execute( "*", $itemtype, "*" );
1382 $irule = $sth->fetchrow_hashref;
1383 return $irule if defined($irule) ;
1385 $sth->execute( "*", "*", "*" );
1386 $irule = $sth->fetchrow_hashref;
1387 return $irule if defined($irule) ;
1389 # if no rule matches,
1390 return undef;
1393 =head2 GetBranchBorrowerCircRule
1395 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1397 Retrieves circulation rule attributes that apply to the given
1398 branch and patron category, regardless of item type.
1399 The return value is a hashref containing the following key:
1401 maxissueqty - maximum number of loans that a
1402 patron of the given category can have at the given
1403 branch. If the value is undef, no limit.
1405 This will first check for a specific branch and
1406 category match from branch_borrower_circ_rules.
1408 If no rule is found, it will then check default_branch_circ_rules
1409 (same branch, default category). If no rule is found,
1410 it will then check default_borrower_circ_rules (default
1411 branch, same category), then failing that, default_circ_rules
1412 (default branch, default category).
1414 If no rule has been found in the database, it will default to
1415 the buillt in rule:
1417 maxissueqty - undef
1419 C<$branchcode> and C<$categorycode> should contain the
1420 literal branch code and patron category code, respectively - no
1421 wildcards.
1423 =cut
1425 sub GetBranchBorrowerCircRule {
1426 my $branchcode = shift;
1427 my $categorycode = shift;
1429 my $branch_cat_query = "SELECT maxissueqty
1430 FROM branch_borrower_circ_rules
1431 WHERE branchcode = ?
1432 AND categorycode = ?";
1433 my $dbh = C4::Context->dbh();
1434 my $sth = $dbh->prepare($branch_cat_query);
1435 $sth->execute($branchcode, $categorycode);
1436 my $result;
1437 if ($result = $sth->fetchrow_hashref()) {
1438 return $result;
1441 # try same branch, default borrower category
1442 my $branch_query = "SELECT maxissueqty
1443 FROM default_branch_circ_rules
1444 WHERE branchcode = ?";
1445 $sth = $dbh->prepare($branch_query);
1446 $sth->execute($branchcode);
1447 if ($result = $sth->fetchrow_hashref()) {
1448 return $result;
1451 # try default branch, same borrower category
1452 my $category_query = "SELECT maxissueqty
1453 FROM default_borrower_circ_rules
1454 WHERE categorycode = ?";
1455 $sth = $dbh->prepare($category_query);
1456 $sth->execute($categorycode);
1457 if ($result = $sth->fetchrow_hashref()) {
1458 return $result;
1461 # try default branch, default borrower category
1462 my $default_query = "SELECT maxissueqty
1463 FROM default_circ_rules";
1464 $sth = $dbh->prepare($default_query);
1465 $sth->execute();
1466 if ($result = $sth->fetchrow_hashref()) {
1467 return $result;
1470 # built-in default circulation rule
1471 return {
1472 maxissueqty => undef,
1476 =head2 GetBranchItemRule
1478 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1480 Retrieves circulation rule attributes that apply to the given
1481 branch and item type, regardless of patron category.
1483 The return value is a hashref containing the following keys:
1485 holdallowed => Hold policy for this branch and itemtype. Possible values:
1486 0: No holds allowed.
1487 1: Holds allowed only by patrons that have the same homebranch as the item.
1488 2: Holds allowed from any patron.
1490 returnbranch => branch to which to return item. Possible values:
1491 noreturn: do not return, let item remain where checked in (floating collections)
1492 homebranch: return to item's home branch
1494 This searches branchitemrules in the following order:
1496 * Same branchcode and itemtype
1497 * Same branchcode, itemtype '*'
1498 * branchcode '*', same itemtype
1499 * branchcode and itemtype '*'
1501 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1503 =cut
1505 sub GetBranchItemRule {
1506 my ( $branchcode, $itemtype ) = @_;
1507 my $dbh = C4::Context->dbh();
1508 my $result = {};
1510 my @attempts = (
1511 ['SELECT holdallowed, returnbranch
1512 FROM branch_item_rules
1513 WHERE branchcode = ?
1514 AND itemtype = ?', $branchcode, $itemtype],
1515 ['SELECT holdallowed, returnbranch
1516 FROM default_branch_circ_rules
1517 WHERE branchcode = ?', $branchcode],
1518 ['SELECT holdallowed, returnbranch
1519 FROM default_branch_item_rules
1520 WHERE itemtype = ?', $itemtype],
1521 ['SELECT holdallowed, returnbranch
1522 FROM default_circ_rules'],
1525 foreach my $attempt (@attempts) {
1526 my ($query, @bind_params) = @{$attempt};
1527 my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1528 or next;
1530 # Since branch/category and branch/itemtype use the same per-branch
1531 # defaults tables, we have to check that the key we want is set, not
1532 # just that a row was returned
1533 $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
1534 $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1537 # built-in default circulation rule
1538 $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1539 $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1541 return $result;
1544 =head2 AddReturn
1546 ($doreturn, $messages, $iteminformation, $borrower) =
1547 &AddReturn($barcode, $branch, $exemptfine, $dropbox);
1549 Returns a book.
1551 =over 4
1553 =item C<$barcode> is the bar code of the book being returned.
1555 =item C<$branch> is the code of the branch where the book is being returned.
1557 =item C<$exemptfine> indicates that overdue charges for the item will be
1558 removed.
1560 =item C<$dropbox> indicates that the check-in date is assumed to be
1561 yesterday, or the last non-holiday as defined in C4::Calendar . If
1562 overdue charges are applied and C<$dropbox> is true, the last charge
1563 will be removed. This assumes that the fines accrual script has run
1564 for _today_.
1566 =back
1568 C<&AddReturn> returns a list of four items:
1570 C<$doreturn> is true iff the return succeeded.
1572 C<$messages> is a reference-to-hash giving feedback on the operation.
1573 The keys of the hash are:
1575 =over 4
1577 =item C<BadBarcode>
1579 No item with this barcode exists. The value is C<$barcode>.
1581 =item C<NotIssued>
1583 The book is not currently on loan. The value is C<$barcode>.
1585 =item C<IsPermanent>
1587 The book's home branch is a permanent collection. If you have borrowed
1588 this book, you are not allowed to return it. The value is the code for
1589 the book's home branch.
1591 =item C<wthdrawn>
1593 This book has been withdrawn/cancelled. The value should be ignored.
1595 =item C<Wrongbranch>
1597 This book has was returned to the wrong branch. The value is a hashref
1598 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1599 contain the branchcode of the incorrect and correct return library, respectively.
1601 =item C<ResFound>
1603 The item was reserved. The value is a reference-to-hash whose keys are
1604 fields from the reserves table of the Koha database, and
1605 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1606 either C<Waiting>, C<Reserved>, or 0.
1608 =back
1610 C<$iteminformation> is a reference-to-hash, giving information about the
1611 returned item from the issues table.
1613 C<$borrower> is a reference-to-hash, giving information about the
1614 patron who last borrowed the book.
1616 =cut
1618 sub AddReturn {
1619 my ( $barcode, $branch, $exemptfine, $dropbox ) = @_;
1621 if ($branch and not GetBranchDetail($branch)) {
1622 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1623 undef $branch;
1625 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1626 my $messages;
1627 my $borrower;
1628 my $biblio;
1629 my $doreturn = 1;
1630 my $validTransfert = 0;
1631 my $stat_type = 'return';
1633 # get information on item
1634 my $itemnumber = GetItemnumberFromBarcode( $barcode );
1635 unless ($itemnumber) {
1636 return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out.
1638 my $issue = GetItemIssue($itemnumber);
1639 # warn Dumper($iteminformation);
1640 if ($issue and $issue->{borrowernumber}) {
1641 $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1642 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n"
1643 . Dumper($issue) . "\n";
1644 } else {
1645 $messages->{'NotIssued'} = $barcode;
1646 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1647 $doreturn = 0;
1648 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1649 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1650 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1651 $messages->{'LocalUse'} = 1;
1652 $stat_type = 'localuse';
1656 my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1657 # full item data, but no borrowernumber or checkout info (no issue)
1658 # we know GetItem should work because GetItemnumberFromBarcode worked
1659 my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1660 # get the proper branch to which to return the item
1661 $hbr = $item->{$hbr} || $branch ;
1662 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1664 my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
1666 # check if the book is in a permanent collection....
1667 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1668 if ( $hbr ) {
1669 my $branches = GetBranches(); # a potentially expensive call for a non-feature.
1670 $branches->{$hbr}->{PE} and $messages->{'IsPermanent'} = $hbr;
1673 # check if the return is allowed at this branch
1674 my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1675 unless ($returnallowed){
1676 $messages->{'Wrongbranch'} = {
1677 Wrongbranch => $branch,
1678 Rightbranch => $message
1680 $doreturn = 0;
1681 return ( $doreturn, $messages, $issue, $borrower );
1684 if ( $item->{'wthdrawn'} ) { # book has been cancelled
1685 $messages->{'wthdrawn'} = 1;
1686 $doreturn = 0;
1689 # case of a return of document (deal with issues and holdingbranch)
1690 my $today = DateTime->now( time_zone => C4::Context->tz() );
1691 if ($doreturn) {
1692 my $datedue = $issue->{date_due};
1693 $borrower or warn "AddReturn without current borrower";
1694 my $circControlBranch;
1695 if ($dropbox) {
1696 # define circControlBranch only if dropbox mode is set
1697 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1698 # FIXME: check issuedate > returndate, factoring in holidays
1699 #$circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );;
1700 $circControlBranch = _GetCircControlBranch($item,$borrower);
1701 $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $today ) == -1 ? 1 : 0;
1704 if ($borrowernumber) {
1705 if($issue->{'overdue'}){
1706 my ( $amount, $type, $unitcounttotal ) = C4::Overdues::CalcFine( $item, $borrower->{categorycode},$branch, $datedue, $today );
1707 $type ||= q{};
1708 if ( $amount > 0 && ( C4::Context->preference('finesMode') eq 'production' )) {
1709 C4::Overdues::UpdateFine(
1710 $issue->{itemnumber},
1711 $issue->{borrowernumber},
1712 $amount, $type, output_pref($datedue)
1716 MarkIssueReturned($borrowernumber, $item->{'itemnumber'}, $circControlBranch, '', $borrower->{'privacy'});
1717 $messages->{'WasReturned'} = 1; # FIXME is the "= 1" right? This could be the borrower hash.
1720 ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1723 # the holdingbranch is updated if the document is returned to another location.
1724 # this is always done regardless of whether the item was on loan or not
1725 if ($item->{'holdingbranch'} ne $branch) {
1726 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1727 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1729 ModDateLastSeen( $item->{'itemnumber'} );
1731 # check if we have a transfer for this document
1732 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1734 # if we have a transfer to do, we update the line of transfers with the datearrived
1735 if ($datesent) {
1736 if ( $tobranch eq $branch ) {
1737 my $sth = C4::Context->dbh->prepare(
1738 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1740 $sth->execute( $item->{'itemnumber'} );
1741 # if we have a reservation with valid transfer, we can set it's status to 'W'
1742 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1743 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1744 } else {
1745 $messages->{'WrongTransfer'} = $tobranch;
1746 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1748 $validTransfert = 1;
1749 } else {
1750 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1753 # fix up the accounts.....
1754 if ($item->{'itemlost'}) {
1755 _FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode); # can tolerate undef $borrowernumber
1756 $messages->{'WasLost'} = 1;
1759 # fix up the overdues in accounts...
1760 if ($borrowernumber) {
1761 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
1762 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
1764 if ( $issue->{overdue} && $issue->{date_due} ) {
1765 # fix fine days
1766 my $debardate =
1767 _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
1768 $messages->{Debarred} = $debardate if ($debardate);
1772 # find reserves.....
1773 # if we don't have a reserve with the status W, we launch the Checkreserves routine
1774 my ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
1775 if ($resfound) {
1776 $resrec->{'ResFound'} = $resfound;
1777 $messages->{'ResFound'} = $resrec;
1780 # update stats?
1781 # Record the fact that this book was returned.
1782 UpdateStats(
1783 $branch, $stat_type, '0', '',
1784 $item->{'itemnumber'},
1785 $biblio->{'itemtype'},
1786 $borrowernumber, undef, $item->{'ccode'}
1789 # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
1790 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1791 my %conditions = (
1792 branchcode => $branch,
1793 categorycode => $borrower->{categorycode},
1794 item_type => $item->{itype},
1795 notification => 'CHECKIN',
1797 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
1798 SendCirculationAlert({
1799 type => 'CHECKIN',
1800 item => $item,
1801 borrower => $borrower,
1802 branch => $branch,
1806 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'biblionumber'})
1807 if C4::Context->preference("ReturnLog");
1809 # FIXME: make this comment intelligible.
1810 #adding message if holdingbranch is non equal a userenv branch to return the document to homebranch
1811 #we check, if we don't have reserv or transfert for this document, if not, return it to homebranch .
1813 if (($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $hbr) and not $messages->{'WrongTransfer'}){
1814 if ( C4::Context->preference("AutomaticItemReturn" ) or
1815 (C4::Context->preference("UseBranchTransferLimits") and
1816 ! IsBranchTransferAllowed($branch, $hbr, $item->{C4::Context->preference("BranchTransferLimitsType")} )
1817 )) {
1818 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $hbr;
1819 $debug and warn "item: " . Dumper($item);
1820 ModItemTransfer($item->{'itemnumber'}, $branch, $hbr);
1821 $messages->{'WasTransfered'} = 1;
1822 } else {
1823 $messages->{'NeedsTransfer'} = 1; # TODO: instead of 1, specify branchcode that the transfer SHOULD go to, $item->{homebranch}
1826 return ( $doreturn, $messages, $issue, $borrower );
1829 =head2 MarkIssueReturned
1831 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
1833 Unconditionally marks an issue as being returned by
1834 moving the C<issues> row to C<old_issues> and
1835 setting C<returndate> to the current date, or
1836 the last non-holiday date of the branccode specified in
1837 C<dropbox_branch> . Assumes you've already checked that
1838 it's safe to do this, i.e. last non-holiday > issuedate.
1840 if C<$returndate> is specified (in iso format), it is used as the date
1841 of the return. It is ignored when a dropbox_branch is passed in.
1843 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
1844 the old_issue is immediately anonymised
1846 Ideally, this function would be internal to C<C4::Circulation>,
1847 not exported, but it is currently needed by one
1848 routine in C<C4::Accounts>.
1850 =cut
1852 sub MarkIssueReturned {
1853 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
1855 my $dbh = C4::Context->dbh;
1856 my $query = 'UPDATE issues SET returndate=';
1857 my @bind;
1858 if ($dropbox_branch) {
1859 my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
1860 my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
1861 $query .= ' ? ';
1862 push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
1863 } elsif ($returndate) {
1864 $query .= ' ? ';
1865 push @bind, $returndate;
1866 } else {
1867 $query .= ' now() ';
1869 $query .= ' WHERE borrowernumber = ? AND itemnumber = ?';
1870 push @bind, $borrowernumber, $itemnumber;
1871 # FIXME transaction
1872 my $sth_upd = $dbh->prepare($query);
1873 $sth_upd->execute(@bind);
1874 my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
1875 WHERE borrowernumber = ?
1876 AND itemnumber = ?');
1877 $sth_copy->execute($borrowernumber, $itemnumber);
1878 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
1879 if ( $privacy == 2) {
1880 # The default of 0 does not work due to foreign key constraints
1881 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
1882 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
1883 my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
1884 WHERE borrowernumber = ?
1885 AND itemnumber = ?");
1886 $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
1888 my $sth_del = $dbh->prepare("DELETE FROM issues
1889 WHERE borrowernumber = ?
1890 AND itemnumber = ?");
1891 $sth_del->execute($borrowernumber, $itemnumber);
1894 =head2 _debar_user_on_return
1896 _debar_user_on_return($borrower, $item, $datedue, today);
1898 C<$borrower> borrower hashref
1900 C<$item> item hashref
1902 C<$datedue> date due DateTime object
1904 C<$today> DateTime object representing the return time
1906 Internal function, called only by AddReturn that calculates and updates
1907 the user fine days, and debars him if necessary.
1909 Should only be called for overdue returns
1911 =cut
1913 sub _debar_user_on_return {
1914 my ( $borrower, $item, $dt_due, $dt_today ) = @_;
1916 my $branchcode = _GetCircControlBranch( $item, $borrower );
1917 my $calendar = Koha::Calendar->new( branchcode => $branchcode );
1919 # $deltadays is a DateTime::Duration object
1920 my $deltadays = $calendar->days_between( $dt_due, $dt_today );
1922 my $circcontrol = C4::Context::preference('CircControl');
1923 my $issuingrule =
1924 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
1925 my $finedays = $issuingrule->{finedays};
1926 my $unit = $issuingrule->{lengthunit};
1928 if ($finedays) {
1930 # finedays is in days, so hourly loans must multiply by 24
1931 # thus 1 hour late equals 1 day suspension * finedays rate
1932 $finedays = $finedays * 24 if ( $unit eq 'hours' );
1934 # grace period is measured in the same units as the loan
1935 my $grace =
1936 DateTime::Duration->new( $unit => $issuingrule->{firstremind} );
1937 if ( $deltadays->subtract($grace)->is_positive() ) {
1939 my $new_debar_dt =
1940 $dt_today->clone()->add_duration( $deltadays * $finedays );
1941 if ( $borrower->{debarred} ) {
1942 my $borrower_debar_dt = dt_from_string( $borrower->{debarred} );
1944 # Update patron only if new date > old
1945 if ( DateTime->compare( $borrower_debar_dt, $new_debar_dt ) !=
1946 -1 )
1948 return;
1952 C4::Members::DebarMember( $borrower->{borrowernumber},
1953 $new_debar_dt->ymd() );
1954 return $new_debar_dt->ymd();
1957 return;
1960 =head2 _FixOverduesOnReturn
1962 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
1964 C<$brn> borrowernumber
1966 C<$itm> itemnumber
1968 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
1969 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
1971 Internal function, called only by AddReturn
1973 =cut
1975 sub _FixOverduesOnReturn {
1976 my ($borrowernumber, $item);
1977 unless ($borrowernumber = shift) {
1978 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
1979 return;
1981 unless ($item = shift) {
1982 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
1983 return;
1985 my ($exemptfine, $dropbox) = @_;
1986 my $dbh = C4::Context->dbh;
1988 # check for overdue fine
1989 my $sth = $dbh->prepare(
1990 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
1992 $sth->execute( $borrowernumber, $item );
1994 # alter fine to show that the book has been returned
1995 my $data = $sth->fetchrow_hashref;
1996 return 0 unless $data; # no warning, there's just nothing to fix
1998 my $uquery;
1999 my @bind = ($data->{'accountlines_id'});
2000 if ($exemptfine) {
2001 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2002 if (C4::Context->preference("FinesLog")) {
2003 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2005 } elsif ($dropbox && $data->{lastincrement}) {
2006 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2007 my $amt = $data->{amount} - $data->{lastincrement} ;
2008 if (C4::Context->preference("FinesLog")) {
2009 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2011 $uquery = "update accountlines set accounttype='F' ";
2012 if($outstanding >= 0 && $amt >=0) {
2013 $uquery .= ", amount = ? , amountoutstanding=? ";
2014 unshift @bind, ($amt, $outstanding) ;
2016 } else {
2017 $uquery = "update accountlines set accounttype='F' ";
2019 $uquery .= " where (accountlines_id = ?)";
2020 my $usth = $dbh->prepare($uquery);
2021 return $usth->execute(@bind);
2024 =head2 _FixAccountForLostAndReturned
2026 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2028 Calculates the charge for a book lost and returned.
2030 Internal function, not exported, called only by AddReturn.
2032 FIXME: This function reflects how inscrutable fines logic is. Fix both.
2033 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
2035 =cut
2037 sub _FixAccountForLostAndReturned {
2038 my $itemnumber = shift or return;
2039 my $borrowernumber = @_ ? shift : undef;
2040 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
2041 my $dbh = C4::Context->dbh;
2042 # check for charge made for lost book
2043 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2044 $sth->execute($itemnumber);
2045 my $data = $sth->fetchrow_hashref;
2046 $data or return; # bail if there is nothing to do
2047 $data->{accounttype} eq 'W' and return; # Written off
2049 # writeoff this amount
2050 my $offset;
2051 my $amount = $data->{'amount'};
2052 my $acctno = $data->{'accountno'};
2053 my $amountleft; # Starts off undef/zero.
2054 if ($data->{'amountoutstanding'} == $amount) {
2055 $offset = $data->{'amount'};
2056 $amountleft = 0; # Hey, it's zero here, too.
2057 } else {
2058 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
2059 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
2061 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2062 WHERE (accountlines_id = ?)");
2063 $usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
2064 #check if any credit is left if so writeoff other accounts
2065 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2066 $amountleft *= -1 if ($amountleft < 0);
2067 if ($amountleft > 0) {
2068 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2069 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
2070 $msth->execute($data->{'borrowernumber'});
2071 # offset transactions
2072 my $newamtos;
2073 my $accdata;
2074 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2075 if ($accdata->{'amountoutstanding'} < $amountleft) {
2076 $newamtos = 0;
2077 $amountleft -= $accdata->{'amountoutstanding'};
2078 } else {
2079 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2080 $amountleft = 0;
2082 my $thisacct = $accdata->{'accountlines_id'};
2083 # FIXME: move prepares outside while loop!
2084 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2085 WHERE (accountlines_id = ?)");
2086 $usth->execute($newamtos,'$thisacct'); # FIXME: '$thisacct' is a string literal!
2087 $usth = $dbh->prepare("INSERT INTO accountoffsets
2088 (borrowernumber, accountno, offsetaccount, offsetamount)
2089 VALUES
2090 (?,?,?,?)");
2091 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2093 $msth->finish; # $msth might actually have data left
2095 $amountleft *= -1 if ($amountleft > 0);
2096 my $desc = "Item Returned " . $item_id;
2097 $usth = $dbh->prepare("INSERT INTO accountlines
2098 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2099 VALUES (?,?,now(),?,?,'CR',?)");
2100 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2101 if ($borrowernumber) {
2102 # FIXME: same as query above. use 1 sth for both
2103 $usth = $dbh->prepare("INSERT INTO accountoffsets
2104 (borrowernumber, accountno, offsetaccount, offsetamount)
2105 VALUES (?,?,?,?)");
2106 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2108 ModItem({ paidfor => '' }, undef, $itemnumber);
2109 return;
2112 =head2 _GetCircControlBranch
2114 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2116 Internal function :
2118 Return the library code to be used to determine which circulation
2119 policy applies to a transaction. Looks up the CircControl and
2120 HomeOrHoldingBranch system preferences.
2122 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2124 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2126 =cut
2128 sub _GetCircControlBranch {
2129 my ($item, $borrower) = @_;
2130 my $circcontrol = C4::Context->preference('CircControl');
2131 my $branch;
2133 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2134 $branch= C4::Context->userenv->{'branch'};
2135 } elsif ($circcontrol eq 'PatronLibrary') {
2136 $branch=$borrower->{branchcode};
2137 } else {
2138 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2139 $branch = $item->{$branchfield};
2140 # default to item home branch if holdingbranch is used
2141 # and is not defined
2142 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2143 $branch = $item->{homebranch};
2146 return $branch;
2154 =head2 GetItemIssue
2156 $issue = &GetItemIssue($itemnumber);
2158 Returns patron currently having a book, or undef if not checked out.
2160 C<$itemnumber> is the itemnumber.
2162 C<$issue> is a hashref of the row from the issues table.
2164 =cut
2166 sub GetItemIssue {
2167 my ($itemnumber) = @_;
2168 return unless $itemnumber;
2169 my $sth = C4::Context->dbh->prepare(
2170 "SELECT *
2171 FROM issues
2172 LEFT JOIN items ON issues.itemnumber=items.itemnumber
2173 WHERE issues.itemnumber=?");
2174 $sth->execute($itemnumber);
2175 my $data = $sth->fetchrow_hashref;
2176 return unless $data;
2177 $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql');
2178 $data->{issuedate}->truncate(to => 'minute');
2179 $data->{date_due} = dt_from_string($data->{date_due}, 'sql');
2180 $data->{date_due}->truncate(to => 'minute');
2181 my $dt = DateTime->now( time_zone => C4::Context->tz)->truncate( to => 'minute');
2182 $data->{'overdue'} = DateTime->compare($data->{'date_due'}, $dt ) == -1 ? 1 : 0;
2183 return $data;
2186 =head2 GetOpenIssue
2188 $issue = GetOpenIssue( $itemnumber );
2190 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2192 C<$itemnumber> is the item's itemnumber
2194 Returns a hashref
2196 =cut
2198 sub GetOpenIssue {
2199 my ( $itemnumber ) = @_;
2201 my $dbh = C4::Context->dbh;
2202 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2203 $sth->execute( $itemnumber );
2204 my $issue = $sth->fetchrow_hashref();
2205 return $issue;
2208 =head2 GetItemIssues
2210 $issues = &GetItemIssues($itemnumber, $history);
2212 Returns patrons that have issued a book
2214 C<$itemnumber> is the itemnumber
2215 C<$history> is false if you just want the current "issuer" (if any)
2216 and true if you want issues history from old_issues also.
2218 Returns reference to an array of hashes
2220 =cut
2222 sub GetItemIssues {
2223 my ( $itemnumber, $history ) = @_;
2225 my $today = DateTime->now( time_zome => C4::Context->tz); # get today date
2226 $today->truncate( to => 'minute' );
2227 my $sql = "SELECT * FROM issues
2228 JOIN borrowers USING (borrowernumber)
2229 JOIN items USING (itemnumber)
2230 WHERE issues.itemnumber = ? ";
2231 if ($history) {
2232 $sql .= "UNION ALL
2233 SELECT * FROM old_issues
2234 LEFT JOIN borrowers USING (borrowernumber)
2235 JOIN items USING (itemnumber)
2236 WHERE old_issues.itemnumber = ? ";
2238 $sql .= "ORDER BY date_due DESC";
2239 my $sth = C4::Context->dbh->prepare($sql);
2240 if ($history) {
2241 $sth->execute($itemnumber, $itemnumber);
2242 } else {
2243 $sth->execute($itemnumber);
2245 my $results = $sth->fetchall_arrayref({});
2246 foreach (@$results) {
2247 my $date_due = dt_from_string($_->{date_due},'sql');
2248 $date_due->truncate( to => 'minute' );
2250 $_->{overdue} = (DateTime->compare($date_due, $today) == -1) ? 1 : 0;
2252 return $results;
2255 =head2 GetBiblioIssues
2257 $issues = GetBiblioIssues($biblionumber);
2259 this function get all issues from a biblionumber.
2261 Return:
2262 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2263 tables issues and the firstname,surname & cardnumber from borrowers.
2265 =cut
2267 sub GetBiblioIssues {
2268 my $biblionumber = shift;
2269 return undef unless $biblionumber;
2270 my $dbh = C4::Context->dbh;
2271 my $query = "
2272 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2273 FROM issues
2274 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2275 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2276 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2277 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2278 WHERE biblio.biblionumber = ?
2279 UNION ALL
2280 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2281 FROM old_issues
2282 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2283 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2284 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2285 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2286 WHERE biblio.biblionumber = ?
2287 ORDER BY timestamp
2289 my $sth = $dbh->prepare($query);
2290 $sth->execute($biblionumber, $biblionumber);
2292 my @issues;
2293 while ( my $data = $sth->fetchrow_hashref ) {
2294 push @issues, $data;
2296 return \@issues;
2299 =head2 GetUpcomingDueIssues
2301 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2303 =cut
2305 sub GetUpcomingDueIssues {
2306 my $params = shift;
2308 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2309 my $dbh = C4::Context->dbh;
2311 my $statement = <<END_SQL;
2312 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2313 FROM issues
2314 LEFT JOIN items USING (itemnumber)
2315 LEFT OUTER JOIN branches USING (branchcode)
2316 WhERE returndate is NULL
2317 AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ?
2318 END_SQL
2320 my @bind_parameters = ( $params->{'days_in_advance'} );
2322 my $sth = $dbh->prepare( $statement );
2323 $sth->execute( @bind_parameters );
2324 my $upcoming_dues = $sth->fetchall_arrayref({});
2325 $sth->finish;
2327 return $upcoming_dues;
2330 =head2 CanBookBeRenewed
2332 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2334 Find out whether a borrowed item may be renewed.
2336 C<$dbh> is a DBI handle to the Koha database.
2338 C<$borrowernumber> is the borrower number of the patron who currently
2339 has the item on loan.
2341 C<$itemnumber> is the number of the item to renew.
2343 C<$override_limit>, if supplied with a true value, causes
2344 the limit on the number of times that the loan can be renewed
2345 (as controlled by the item type) to be ignored.
2347 C<$CanBookBeRenewed> returns a true value iff the item may be renewed. The
2348 item must currently be on loan to the specified borrower; renewals
2349 must be allowed for the item's type; and the borrower must not have
2350 already renewed the loan. $error will contain the reason the renewal can not proceed
2352 =cut
2354 sub CanBookBeRenewed {
2356 # check renewal status
2357 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2358 my $dbh = C4::Context->dbh;
2359 my $renews = 1;
2360 my $renewokay = 0;
2361 my $error;
2363 # Look in the issues table for this item, lent to this borrower,
2364 # and not yet returned.
2366 # Look in the issues table for this item, lent to this borrower,
2367 # and not yet returned.
2368 my %branch = (
2369 'ItemHomeLibrary' => 'items.homebranch',
2370 'PickupLibrary' => 'items.holdingbranch',
2371 'PatronLibrary' => 'borrowers.branchcode'
2373 my $controlbranch = $branch{C4::Context->preference('CircControl')};
2374 my $itype = C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype';
2376 my $sthcount = $dbh->prepare("
2377 SELECT
2378 borrowers.categorycode, biblioitems.itemtype, issues.renewals, renewalsallowed, $controlbranch
2379 FROM issuingrules,
2380 issues
2381 LEFT JOIN items USING (itemnumber)
2382 LEFT JOIN borrowers USING (borrowernumber)
2383 LEFT JOIN biblioitems USING (biblioitemnumber)
2385 WHERE
2386 (issuingrules.categorycode = borrowers.categorycode OR issuingrules.categorycode = '*')
2388 (issuingrules.itemtype = $itype OR issuingrules.itemtype = '*')
2390 (issuingrules.branchcode = $controlbranch OR issuingrules.branchcode = '*')
2391 AND
2392 borrowernumber = ?
2394 itemnumber = ?
2395 ORDER BY
2396 issuingrules.categorycode desc,
2397 issuingrules.itemtype desc,
2398 issuingrules.branchcode desc
2399 LIMIT 1;
2402 $sthcount->execute( $borrowernumber, $itemnumber );
2403 if ( my $data1 = $sthcount->fetchrow_hashref ) {
2405 if ( ( $data1->{renewalsallowed} && $data1->{renewalsallowed} > $data1->{renewals} ) || $override_limit ) {
2406 $renewokay = 1;
2408 else {
2409 $error="too_many";
2412 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2413 if ($resfound) {
2414 $renewokay = 0;
2415 $error="on_reserve"
2419 return ($renewokay,$error);
2422 =head2 AddRenewal
2424 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2426 Renews a loan.
2428 C<$borrowernumber> is the borrower number of the patron who currently
2429 has the item.
2431 C<$itemnumber> is the number of the item to renew.
2433 C<$branch> is the library where the renewal took place (if any).
2434 The library that controls the circ policies for the renewal is retrieved from the issues record.
2436 C<$datedue> can be a C4::Dates object used to set the due date.
2438 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2439 this parameter is not supplied, lastreneweddate is set to the current date.
2441 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2442 from the book's item type.
2444 =cut
2446 sub AddRenewal {
2447 my $borrowernumber = shift or return undef;
2448 my $itemnumber = shift or return undef;
2449 my $branch = shift;
2450 my $datedue = shift;
2451 my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2452 my $item = GetItem($itemnumber) or return undef;
2453 my $biblio = GetBiblioFromItemNumber($itemnumber) or return undef;
2455 my $dbh = C4::Context->dbh;
2456 # Find the issues record for this book
2457 my $sth =
2458 $dbh->prepare("SELECT * FROM issues
2459 WHERE borrowernumber=?
2460 AND itemnumber=?"
2462 $sth->execute( $borrowernumber, $itemnumber );
2463 my $issuedata = $sth->fetchrow_hashref;
2464 $sth->finish;
2465 if(defined $datedue && ref $datedue ne 'DateTime' ) {
2466 carp 'Invalid date passed to AddRenewal.';
2467 return;
2469 # If the due date wasn't specified, calculate it by adding the
2470 # book's loan length to today's date or the current due date
2471 # based on the value of the RenewalPeriodBase syspref.
2472 unless ($datedue) {
2474 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return undef;
2475 my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2477 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2478 $issuedata->{date_due} :
2479 DateTime->now( time_zone => C4::Context->tz());
2480 $datedue = CalcDateDue($datedue,$itemtype,$issuedata->{'branchcode'},$borrower);
2483 # Update the issues record to have the new due date, and a new count
2484 # of how many times it has been renewed.
2485 my $renews = $issuedata->{'renewals'} + 1;
2486 $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2487 WHERE borrowernumber=?
2488 AND itemnumber=?"
2491 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2492 $sth->finish;
2494 # Update the renewal count on the item, and tell zebra to reindex
2495 $renews = $biblio->{'renewals'} + 1;
2496 ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2498 # Charge a new rental fee, if applicable?
2499 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2500 if ( $charge > 0 ) {
2501 my $accountno = getnextacctno( $borrowernumber );
2502 my $item = GetBiblioFromItemNumber($itemnumber);
2503 my $manager_id = 0;
2504 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2505 $sth = $dbh->prepare(
2506 "INSERT INTO accountlines
2507 (date, borrowernumber, accountno, amount, manager_id,
2508 description,accounttype, amountoutstanding, itemnumber)
2509 VALUES (now(),?,?,?,?,?,?,?,?)"
2511 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2512 "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2513 'Rent', $charge, $itemnumber );
2515 # Log the renewal
2516 UpdateStats( $branch, 'renew', $charge, '', $itemnumber, $item->{itype}, $borrowernumber, undef, $item->{'ccode'});
2517 return $datedue;
2520 sub GetRenewCount {
2521 # check renewal status
2522 my ( $bornum, $itemno ) = @_;
2523 my $dbh = C4::Context->dbh;
2524 my $renewcount = 0;
2525 my $renewsallowed = 0;
2526 my $renewsleft = 0;
2528 my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2529 my $item = GetItem($itemno);
2531 # Look in the issues table for this item, lent to this borrower,
2532 # and not yet returned.
2534 # FIXME - I think this function could be redone to use only one SQL call.
2535 my $sth = $dbh->prepare(
2536 "select * from issues
2537 where (borrowernumber = ?)
2538 and (itemnumber = ?)"
2540 $sth->execute( $bornum, $itemno );
2541 my $data = $sth->fetchrow_hashref;
2542 $renewcount = $data->{'renewals'} if $data->{'renewals'};
2543 $sth->finish;
2544 # $item and $borrower should be calculated
2545 my $branchcode = _GetCircControlBranch($item, $borrower);
2547 my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2549 $renewsallowed = $issuingrule->{'renewalsallowed'};
2550 $renewsleft = $renewsallowed - $renewcount;
2551 if($renewsleft < 0){ $renewsleft = 0; }
2552 return ( $renewcount, $renewsallowed, $renewsleft );
2555 =head2 GetIssuingCharges
2557 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
2559 Calculate how much it would cost for a given patron to borrow a given
2560 item, including any applicable discounts.
2562 C<$itemnumber> is the item number of item the patron wishes to borrow.
2564 C<$borrowernumber> is the patron's borrower number.
2566 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
2567 and C<$item_type> is the code for the item's item type (e.g., C<VID>
2568 if it's a video).
2570 =cut
2572 sub GetIssuingCharges {
2574 # calculate charges due
2575 my ( $itemnumber, $borrowernumber ) = @_;
2576 my $charge = 0;
2577 my $dbh = C4::Context->dbh;
2578 my $item_type;
2580 # Get the book's item type and rental charge (via its biblioitem).
2581 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
2582 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
2583 $charge_query .= (C4::Context->preference('item-level_itypes'))
2584 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
2585 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
2587 $charge_query .= ' WHERE items.itemnumber =?';
2589 my $sth = $dbh->prepare($charge_query);
2590 $sth->execute($itemnumber);
2591 if ( my $item_data = $sth->fetchrow_hashref ) {
2592 $item_type = $item_data->{itemtype};
2593 $charge = $item_data->{rentalcharge};
2594 my $branch = C4::Branch::mybranch();
2595 my $discount_query = q|SELECT rentaldiscount,
2596 issuingrules.itemtype, issuingrules.branchcode
2597 FROM borrowers
2598 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
2599 WHERE borrowers.borrowernumber = ?
2600 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2601 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
2602 my $discount_sth = $dbh->prepare($discount_query);
2603 $discount_sth->execute( $borrowernumber, $item_type, $branch );
2604 my $discount_rules = $discount_sth->fetchall_arrayref({});
2605 if (@{$discount_rules}) {
2606 # We may have multiple rules so get the most specific
2607 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
2608 $charge = ( $charge * ( 100 - $discount ) ) / 100;
2612 $sth->finish; # we havent _explicitly_ fetched all rows
2613 return ( $charge, $item_type );
2616 # Select most appropriate discount rule from those returned
2617 sub _get_discount_from_rule {
2618 my ($rules_ref, $branch, $itemtype) = @_;
2619 my $discount;
2621 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
2622 $discount = $rules_ref->[0]->{rentaldiscount};
2623 return (defined $discount) ? $discount : 0;
2625 # could have up to 4 does one match $branch and $itemtype
2626 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
2627 if (@d) {
2628 $discount = $d[0]->{rentaldiscount};
2629 return (defined $discount) ? $discount : 0;
2631 # do we have item type + all branches
2632 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
2633 if (@d) {
2634 $discount = $d[0]->{rentaldiscount};
2635 return (defined $discount) ? $discount : 0;
2637 # do we all item types + this branch
2638 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
2639 if (@d) {
2640 $discount = $d[0]->{rentaldiscount};
2641 return (defined $discount) ? $discount : 0;
2643 # so all and all (surely we wont get here)
2644 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
2645 if (@d) {
2646 $discount = $d[0]->{rentaldiscount};
2647 return (defined $discount) ? $discount : 0;
2649 # none of the above
2650 return 0;
2653 =head2 AddIssuingCharge
2655 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
2657 =cut
2659 sub AddIssuingCharge {
2660 my ( $itemnumber, $borrowernumber, $charge ) = @_;
2661 my $dbh = C4::Context->dbh;
2662 my $nextaccntno = getnextacctno( $borrowernumber );
2663 my $manager_id = 0;
2664 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2665 my $query ="
2666 INSERT INTO accountlines
2667 (borrowernumber, itemnumber, accountno,
2668 date, amount, description, accounttype,
2669 amountoutstanding, manager_id)
2670 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
2672 my $sth = $dbh->prepare($query);
2673 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
2674 $sth->finish;
2677 =head2 GetTransfers
2679 GetTransfers($itemnumber);
2681 =cut
2683 sub GetTransfers {
2684 my ($itemnumber) = @_;
2686 my $dbh = C4::Context->dbh;
2688 my $query = '
2689 SELECT datesent,
2690 frombranch,
2691 tobranch
2692 FROM branchtransfers
2693 WHERE itemnumber = ?
2694 AND datearrived IS NULL
2696 my $sth = $dbh->prepare($query);
2697 $sth->execute($itemnumber);
2698 my @row = $sth->fetchrow_array();
2699 $sth->finish;
2700 return @row;
2703 =head2 GetTransfersFromTo
2705 @results = GetTransfersFromTo($frombranch,$tobranch);
2707 Returns the list of pending transfers between $from and $to branch
2709 =cut
2711 sub GetTransfersFromTo {
2712 my ( $frombranch, $tobranch ) = @_;
2713 return unless ( $frombranch && $tobranch );
2714 my $dbh = C4::Context->dbh;
2715 my $query = "
2716 SELECT itemnumber,datesent,frombranch
2717 FROM branchtransfers
2718 WHERE frombranch=?
2719 AND tobranch=?
2720 AND datearrived IS NULL
2722 my $sth = $dbh->prepare($query);
2723 $sth->execute( $frombranch, $tobranch );
2724 my @gettransfers;
2726 while ( my $data = $sth->fetchrow_hashref ) {
2727 push @gettransfers, $data;
2729 $sth->finish;
2730 return (@gettransfers);
2733 =head2 DeleteTransfer
2735 &DeleteTransfer($itemnumber);
2737 =cut
2739 sub DeleteTransfer {
2740 my ($itemnumber) = @_;
2741 my $dbh = C4::Context->dbh;
2742 my $sth = $dbh->prepare(
2743 "DELETE FROM branchtransfers
2744 WHERE itemnumber=?
2745 AND datearrived IS NULL "
2747 $sth->execute($itemnumber);
2748 $sth->finish;
2751 =head2 AnonymiseIssueHistory
2753 $rows = AnonymiseIssueHistory($date,$borrowernumber)
2755 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
2756 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
2758 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
2759 setting (force delete).
2761 return the number of affected rows.
2763 =cut
2765 sub AnonymiseIssueHistory {
2766 my $date = shift;
2767 my $borrowernumber = shift;
2768 my $dbh = C4::Context->dbh;
2769 my $query = "
2770 UPDATE old_issues
2771 SET borrowernumber = ?
2772 WHERE returndate < ?
2773 AND borrowernumber IS NOT NULL
2776 # The default of 0 does not work due to foreign key constraints
2777 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
2778 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
2779 my @bind_params = ($anonymouspatron, $date);
2780 if (defined $borrowernumber) {
2781 $query .= " AND borrowernumber = ?";
2782 push @bind_params, $borrowernumber;
2783 } else {
2784 $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
2786 my $sth = $dbh->prepare($query);
2787 $sth->execute(@bind_params);
2788 my $rows_affected = $sth->rows; ### doublecheck row count return function
2789 return $rows_affected;
2792 =head2 SendCirculationAlert
2794 Send out a C<check-in> or C<checkout> alert using the messaging system.
2796 B<Parameters>:
2798 =over 4
2800 =item type
2802 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
2804 =item item
2806 Hashref of information about the item being checked in or out.
2808 =item borrower
2810 Hashref of information about the borrower of the item.
2812 =item branch
2814 The branchcode from where the checkout or check-in took place.
2816 =back
2818 B<Example>:
2820 SendCirculationAlert({
2821 type => 'CHECKOUT',
2822 item => $item,
2823 borrower => $borrower,
2824 branch => $branch,
2827 =cut
2829 sub SendCirculationAlert {
2830 my ($opts) = @_;
2831 my ($type, $item, $borrower, $branch) =
2832 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
2833 my %message_name = (
2834 CHECKIN => 'Item_Check_in',
2835 CHECKOUT => 'Item_Checkout',
2837 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
2838 borrowernumber => $borrower->{borrowernumber},
2839 message_name => $message_name{$type},
2841 my $issues_table = ( $type eq 'CHECKOUT' ) ? 'issues' : 'old_issues';
2842 my $letter = C4::Letters::GetPreparedLetter (
2843 module => 'circulation',
2844 letter_code => $type,
2845 branchcode => $branch,
2846 tables => {
2847 $issues_table => $item->{itemnumber},
2848 'items' => $item->{itemnumber},
2849 'biblio' => $item->{biblionumber},
2850 'biblioitems' => $item->{biblionumber},
2851 'borrowers' => $borrower,
2852 'branches' => $branch,
2854 ) or return;
2856 my @transports = keys %{ $borrower_preferences->{transports} };
2857 # warn "no transports" unless @transports;
2858 for (@transports) {
2859 # warn "transport: $_";
2860 my $message = C4::Message->find_last_message($borrower, $type, $_);
2861 if (!$message) {
2862 #warn "create new message";
2863 C4::Message->enqueue($letter, $borrower, $_);
2864 } else {
2865 #warn "append to old message";
2866 $message->append($letter);
2867 $message->update;
2871 return $letter;
2874 =head2 updateWrongTransfer
2876 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
2878 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
2880 =cut
2882 sub updateWrongTransfer {
2883 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
2884 my $dbh = C4::Context->dbh;
2885 # first step validate the actual line of transfert .
2886 my $sth =
2887 $dbh->prepare(
2888 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
2890 $sth->execute($FromLibrary,$itemNumber);
2891 $sth->finish;
2893 # second step create a new line of branchtransfer to the right location .
2894 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
2896 #third step changing holdingbranch of item
2897 UpdateHoldingbranch($FromLibrary,$itemNumber);
2900 =head2 UpdateHoldingbranch
2902 $items = UpdateHoldingbranch($branch,$itmenumber);
2904 Simple methode for updating hodlingbranch in items BDD line
2906 =cut
2908 sub UpdateHoldingbranch {
2909 my ( $branch,$itemnumber ) = @_;
2910 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
2913 =head2 CalcDateDue
2915 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
2917 this function calculates the due date given the start date and configured circulation rules,
2918 checking against the holidays calendar as per the 'useDaysMode' syspref.
2919 C<$startdate> = C4::Dates object representing start date of loan period (assumed to be today)
2920 C<$itemtype> = itemtype code of item in question
2921 C<$branch> = location whose calendar to use
2922 C<$borrower> = Borrower object
2924 =cut
2926 sub CalcDateDue {
2927 my ( $startdate, $itemtype, $branch, $borrower ) = @_;
2929 # loanlength now a href
2930 my $loanlength =
2931 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
2933 my $datedue;
2935 # if globalDueDate ON the datedue is set to that date
2936 if (C4::Context->preference('globalDueDate')
2937 && ( C4::Context->preference('globalDueDate') =~
2938 C4::Dates->regexp('syspref') )
2940 $datedue = dt_from_string(
2941 C4::Context->preference('globalDueDate'),
2942 C4::Context->preference('dateformat')
2944 } else {
2946 # otherwise, calculate the datedue as normal
2947 if ( C4::Context->preference('useDaysMode') eq 'Days' )
2948 { # ignoring calendar
2949 my $dt =
2950 DateTime->now( time_zone => C4::Context->tz() )
2951 ->truncate( to => 'minute' );
2952 if ( $loanlength->{lengthunit} eq 'hours' ) {
2953 $dt->add( hours => $loanlength->{issuelength} );
2954 return $dt;
2955 } else { # days
2956 $dt->add( days => $loanlength->{issuelength} );
2957 $dt->set_hour(23);
2958 $dt->set_minute(59);
2959 return $dt;
2961 } else {
2962 my $dur;
2963 if ($loanlength->{lengthunit} eq 'hours') {
2964 $dur = DateTime::Duration->new( hours => $loanlength->{issuelength});
2966 else { # days
2967 $dur = DateTime::Duration->new( days => $loanlength->{issuelength});
2969 if (ref $startdate ne 'DateTime' ) {
2970 $startdate = dt_from_string($startdate);
2972 my $calendar = Koha::Calendar->new( branchcode => $branch );
2973 $datedue = $calendar->addDate( $startdate, $dur, $loanlength->{lengthunit} );
2974 if ($loanlength->{lengthunit} eq 'days') {
2975 $datedue->set_hour(23);
2976 $datedue->set_minute(59);
2981 # if Hard Due Dates are used, retreive them and apply as necessary
2982 my ( $hardduedate, $hardduedatecompare ) =
2983 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
2984 if ($hardduedate) { # hardduedates are currently dates
2985 $hardduedate->truncate( to => 'minute' );
2986 $hardduedate->set_hour(23);
2987 $hardduedate->set_minute(59);
2988 my $cmp = DateTime->compare( $hardduedate, $datedue );
2990 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
2991 # if the calculated date is before the 'after' Hard Due Date (floor), override
2992 # if the hard due date is set to 'exactly', overrride
2993 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
2994 $datedue = $hardduedate->clone;
2997 # in all other cases, keep the date due as it is
3000 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3001 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3002 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso' );
3003 if ( DateTime->compare( $datedue, $expiry_dt ) == 1 ) {
3004 $datedue = $expiry_dt->clone;
3008 return $datedue;
3012 =head2 CheckRepeatableHolidays
3014 $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
3016 This function checks if the date due is a repeatable holiday
3018 C<$date_due> = returndate calculate with no day check
3019 C<$itemnumber> = itemnumber
3020 C<$branchcode> = localisation of issue
3022 =cut
3024 sub CheckRepeatableHolidays{
3025 my($itemnumber,$week_day,$branchcode)=@_;
3026 my $dbh = C4::Context->dbh;
3027 my $query = qq|SELECT count(*)
3028 FROM repeatable_holidays
3029 WHERE branchcode=?
3030 AND weekday=?|;
3031 my $sth = $dbh->prepare($query);
3032 $sth->execute($branchcode,$week_day);
3033 my $result=$sth->fetchrow;
3034 $sth->finish;
3035 return $result;
3039 =head2 CheckSpecialHolidays
3041 $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
3043 This function check if the date is a special holiday
3045 C<$years> = the years of datedue
3046 C<$month> = the month of datedue
3047 C<$day> = the day of datedue
3048 C<$itemnumber> = itemnumber
3049 C<$branchcode> = localisation of issue
3051 =cut
3053 sub CheckSpecialHolidays{
3054 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
3055 my $dbh = C4::Context->dbh;
3056 my $query=qq|SELECT count(*)
3057 FROM `special_holidays`
3058 WHERE year=?
3059 AND month=?
3060 AND day=?
3061 AND branchcode=?
3063 my $sth = $dbh->prepare($query);
3064 $sth->execute($years,$month,$day,$branchcode);
3065 my $countspecial=$sth->fetchrow ;
3066 $sth->finish;
3067 return $countspecial;
3070 =head2 CheckRepeatableSpecialHolidays
3072 $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
3074 This function check if the date is a repeatble special holidays
3076 C<$month> = the month of datedue
3077 C<$day> = the day of datedue
3078 C<$itemnumber> = itemnumber
3079 C<$branchcode> = localisation of issue
3081 =cut
3083 sub CheckRepeatableSpecialHolidays{
3084 my ($month,$day,$itemnumber,$branchcode) = @_;
3085 my $dbh = C4::Context->dbh;
3086 my $query=qq|SELECT count(*)
3087 FROM `repeatable_holidays`
3088 WHERE month=?
3089 AND day=?
3090 AND branchcode=?
3092 my $sth = $dbh->prepare($query);
3093 $sth->execute($month,$day,$branchcode);
3094 my $countspecial=$sth->fetchrow ;
3095 $sth->finish;
3096 return $countspecial;
3101 sub CheckValidBarcode{
3102 my ($barcode) = @_;
3103 my $dbh = C4::Context->dbh;
3104 my $query=qq|SELECT count(*)
3105 FROM items
3106 WHERE barcode=?
3108 my $sth = $dbh->prepare($query);
3109 $sth->execute($barcode);
3110 my $exist=$sth->fetchrow ;
3111 $sth->finish;
3112 return $exist;
3115 =head2 IsBranchTransferAllowed
3117 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3119 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3121 =cut
3123 sub IsBranchTransferAllowed {
3124 my ( $toBranch, $fromBranch, $code ) = @_;
3126 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3128 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3129 my $dbh = C4::Context->dbh;
3131 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3132 $sth->execute( $toBranch, $fromBranch, $code );
3133 my $limit = $sth->fetchrow_hashref();
3135 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3136 if ( $limit->{'limitId'} ) {
3137 return 0;
3138 } else {
3139 return 1;
3143 =head2 CreateBranchTransferLimit
3145 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3147 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3149 =cut
3151 sub CreateBranchTransferLimit {
3152 my ( $toBranch, $fromBranch, $code ) = @_;
3154 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3156 my $dbh = C4::Context->dbh;
3158 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3159 $sth->execute( $code, $toBranch, $fromBranch );
3162 =head2 DeleteBranchTransferLimits
3164 DeleteBranchTransferLimits($frombranch);
3166 Deletes all the branch transfer limits for one branch
3168 =cut
3170 sub DeleteBranchTransferLimits {
3171 my $branch = shift;
3172 my $dbh = C4::Context->dbh;
3173 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3174 $sth->execute($branch);
3177 sub ReturnLostItem{
3178 my ( $borrowernumber, $itemnum ) = @_;
3180 MarkIssueReturned( $borrowernumber, $itemnum );
3181 my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3182 my $item = C4::Items::GetItem( $itemnum );
3183 my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3184 my @datearr = localtime(time);
3185 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3186 my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3187 ModItem({ paidfor => $old_note."Paid for by $bor $date" }, undef, $itemnum);
3191 sub LostItem{
3192 my ($itemnumber, $mark_returned, $charge_fee) = @_;
3194 my $dbh = C4::Context->dbh();
3195 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3196 FROM issues
3197 JOIN items USING (itemnumber)
3198 JOIN biblio USING (biblionumber)
3199 WHERE issues.itemnumber=?");
3200 $sth->execute($itemnumber);
3201 my $issues=$sth->fetchrow_hashref();
3202 $sth->finish;
3204 # if a borrower lost the item, add a replacement cost to the their record
3205 if ( my $borrowernumber = $issues->{borrowernumber} ){
3206 my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3208 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}")
3209 if $charge_fee;
3210 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3211 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3212 MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3216 sub GetOfflineOperations {
3217 my $dbh = C4::Context->dbh;
3218 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3219 $sth->execute(C4::Context->userenv->{'branch'});
3220 my $results = $sth->fetchall_arrayref({});
3221 $sth->finish;
3222 return $results;
3225 sub GetOfflineOperation {
3226 my $dbh = C4::Context->dbh;
3227 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3228 $sth->execute( shift );
3229 my $result = $sth->fetchrow_hashref;
3230 $sth->finish;
3231 return $result;
3234 sub AddOfflineOperation {
3235 my $dbh = C4::Context->dbh;
3236 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber) VALUES(?,?,?,?,?,?)");
3237 $sth->execute( @_ );
3238 return "Added.";
3241 sub DeleteOfflineOperation {
3242 my $dbh = C4::Context->dbh;
3243 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3244 $sth->execute( shift );
3245 return "Deleted.";
3248 sub ProcessOfflineOperation {
3249 my $operation = shift;
3251 my $report;
3252 if ( $operation->{action} eq 'return' ) {
3253 $report = ProcessOfflineReturn( $operation );
3254 } elsif ( $operation->{action} eq 'issue' ) {
3255 $report = ProcessOfflineIssue( $operation );
3258 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3260 return $report;
3263 sub ProcessOfflineReturn {
3264 my $operation = shift;
3266 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3268 if ( $itemnumber ) {
3269 my $issue = GetOpenIssue( $itemnumber );
3270 if ( $issue ) {
3271 MarkIssueReturned(
3272 $issue->{borrowernumber},
3273 $itemnumber,
3274 undef,
3275 $operation->{timestamp},
3277 ModItem(
3278 { renewals => 0, onloan => undef },
3279 $issue->{'biblionumber'},
3280 $itemnumber
3282 return "Success.";
3283 } else {
3284 return "Item not issued.";
3286 } else {
3287 return "Item not found.";
3291 sub ProcessOfflineIssue {
3292 my $operation = shift;
3294 my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3296 if ( $borrower->{borrowernumber} ) {
3297 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3298 unless ($itemnumber) {
3299 return "Barcode not found.";
3301 my $issue = GetOpenIssue( $itemnumber );
3303 if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3304 MarkIssueReturned(
3305 $issue->{borrowernumber},
3306 $itemnumber,
3307 undef,
3308 $operation->{timestamp},
3311 AddIssue(
3312 $borrower,
3313 $operation->{'barcode'},
3314 undef,
3316 $operation->{timestamp},
3317 undef,
3319 return "Success.";
3320 } else {
3321 return "Borrower not found.";
3327 =head2 TransferSlip
3329 TransferSlip($user_branch, $itemnumber, $to_branch)
3331 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3333 =cut
3335 sub TransferSlip {
3336 my ($branch, $itemnumber, $to_branch) = @_;
3338 my $item = GetItem( $itemnumber )
3339 or return;
3341 my $pulldate = C4::Dates->new();
3343 return C4::Letters::GetPreparedLetter (
3344 module => 'circulation',
3345 letter_code => 'TRANSFERSLIP',
3346 branchcode => $branch,
3347 tables => {
3348 'branches' => $to_branch,
3349 'biblio' => $item->{biblionumber},
3350 'items' => $item,
3358 __END__
3360 =head1 AUTHOR
3362 Koha Development Team <http://koha-community.org/>
3364 =cut