Bug 10120: Adding a syspref to control if overdue charges are updated on return
[koha.git] / C4 / Reserves.pm
blob5ff12e44ec5cec671362cf3988b9366f22e3195b
1 package C4::Reserves;
3 # Copyright 2000-2002 Katipo Communications
4 # 2006 SAN Ouest Provence
5 # 2007-2010 BibLibre Paul POULAIN
6 # 2011 Catalyst IT
8 # This file is part of Koha.
10 # Koha is free software; you can redistribute it and/or modify it under the
11 # terms of the GNU General Public License as published by the Free Software
12 # Foundation; either version 2 of the License, or (at your option) any later
13 # version.
15 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
16 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
17 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License along
20 # with Koha; if not, write to the Free Software Foundation, Inc.,
21 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 use strict;
25 #use warnings; FIXME - Bug 2505
26 use C4::Context;
27 use C4::Biblio;
28 use C4::Members;
29 use C4::Items;
30 use C4::Circulation;
31 use C4::Accounts;
33 # for _koha_notify_reserve
34 use C4::Members::Messaging;
35 use C4::Members qw();
36 use C4::Letters;
37 use C4::Branch qw( GetBranchDetail );
38 use C4::Dates qw( format_date_in_iso );
40 use Koha::DateUtils;
42 use List::MoreUtils qw( firstidx );
44 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
46 =head1 NAME
48 C4::Reserves - Koha functions for dealing with reservation.
50 =head1 SYNOPSIS
52 use C4::Reserves;
54 =head1 DESCRIPTION
56 This modules provides somes functions to deal with reservations.
58 Reserves are stored in reserves table.
59 The following columns contains important values :
60 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
61 =0 : then the reserve is being dealed
62 - found : NULL : means the patron requested the 1st available, and we haven't choosen the item
63 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
64 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
65 F(inished) : the reserve has been completed, and is done
66 - itemnumber : empty : the reserve is still unaffected to an item
67 filled: the reserve is attached to an item
68 The complete workflow is :
69 ==== 1st use case ====
70 patron request a document, 1st available : P >0, F=NULL, I=NULL
71 a library having it run "transfertodo", and clic on the list
72 if there is no transfer to do, the reserve waiting
73 patron can pick it up P =0, F=W, I=filled
74 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
75 The pickup library recieve the book, it check in P =0, F=W, I=filled
76 The patron borrow the book P =0, F=F, I=filled
78 ==== 2nd use case ====
79 patron requests a document, a given item,
80 If pickup is holding branch P =0, F=W, I=filled
81 If transfer needed, write in branchtransfer P =0, F=T, I=filled
82 The pickup library receive the book, it checks it in P =0, F=W, I=filled
83 The patron borrow the book P =0, F=F, I=filled
85 =head1 FUNCTIONS
87 =cut
89 BEGIN {
90 # set the version for version checking
91 $VERSION = 3.07.00.049;
92 require Exporter;
93 @ISA = qw(Exporter);
94 @EXPORT = qw(
95 &AddReserve
97 &GetReservesFromItemnumber
98 &GetReservesFromBiblionumber
99 &GetReservesFromBorrowernumber
100 &GetReservesForBranch
101 &GetReservesToBranch
102 &GetReserveCount
103 &GetReserveFee
104 &GetReserveInfo
105 &GetReserveStatus
107 &GetOtherReserves
109 &ModReserveFill
110 &ModReserveAffect
111 &ModReserve
112 &ModReserveStatus
113 &ModReserveCancelAll
114 &ModReserveMinusPriority
115 &MoveReserve
117 &CheckReserves
118 &CanBookBeReserved
119 &CanItemBeReserved
120 &CancelReserve
121 &CancelExpiredReserves
123 &AutoUnsuspendReserves
125 &IsAvailableForItemLevelRequest
127 &AlterPriority
128 &ToggleLowestPriority
130 &ReserveSlip
131 &ToggleSuspend
132 &SuspendAll
134 @EXPORT_OK = qw( MergeHolds );
137 =head2 AddReserve
139 AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
141 =cut
143 sub AddReserve {
144 my (
145 $branch, $borrowernumber, $biblionumber,
146 $constraint, $bibitems, $priority, $resdate, $expdate, $notes,
147 $title, $checkitem, $found
148 ) = @_;
149 my $fee =
150 GetReserveFee($borrowernumber, $biblionumber, $constraint,
151 $bibitems );
152 my $dbh = C4::Context->dbh;
153 my $const = lc substr( $constraint, 0, 1 );
154 $resdate = format_date_in_iso( $resdate ) if ( $resdate );
155 $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
156 if ($expdate) {
157 $expdate = format_date_in_iso( $expdate );
158 } else {
159 undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
161 if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
162 # Make room in reserves for this before those of a later reserve date
163 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
165 my $waitingdate;
167 # If the reserv had the waiting status, we had the value of the resdate
168 if ( $found eq 'W' ) {
169 $waitingdate = $resdate;
172 #eval {
173 # updates take place here
174 if ( $fee > 0 ) {
175 my $nextacctno = &getnextacctno( $borrowernumber );
176 my $query = qq/
177 INSERT INTO accountlines
178 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
179 VALUES
180 (?,?,now(),?,?,'Res',?)
182 my $usth = $dbh->prepare($query);
183 $usth->execute( $borrowernumber, $nextacctno, $fee,
184 "Reserve Charge - $title", $fee );
187 #if ($const eq 'a'){
188 my $query = qq/
189 INSERT INTO reserves
190 (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
191 priority,reservenotes,itemnumber,found,waitingdate,expirationdate)
192 VALUES
193 (?,?,?,?,?,
194 ?,?,?,?,?,?)
196 my $sth = $dbh->prepare($query);
197 $sth->execute(
198 $borrowernumber, $biblionumber, $resdate, $branch,
199 $const, $priority, $notes, $checkitem,
200 $found, $waitingdate, $expdate
203 # Send e-mail to librarian if syspref is active
204 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
205 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
206 my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode});
207 if ( my $letter = C4::Letters::GetPreparedLetter (
208 module => 'reserves',
209 letter_code => 'HOLDPLACED',
210 branchcode => $branch,
211 tables => {
212 'branches' => $branch_details,
213 'borrowers' => $borrower,
214 'biblio' => $biblionumber,
216 ) ) {
218 my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
220 C4::Letters::EnqueueLetter(
221 { letter => $letter,
222 borrowernumber => $borrowernumber,
223 message_transport_type => 'email',
224 from_address => $admin_email_address,
225 to_address => $admin_email_address,
232 ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value?
233 $query = qq/
234 INSERT INTO reserveconstraints
235 (borrowernumber,biblionumber,reservedate,biblioitemnumber)
236 VALUES
237 (?,?,?,?)
239 $sth = $dbh->prepare($query); # keep prepare outside the loop!
240 foreach (@$bibitems) {
241 $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
244 return; # FIXME: why not have a useful return value?
247 =head2 GetReservesFromBiblionumber
249 ($count, $title_reserves) = &GetReserves($biblionumber);
251 This function gets the list of reservations for one C<$biblionumber>, returning a count
252 of the reserves and an arrayref pointing to the reserves for C<$biblionumber>.
254 =cut
256 sub GetReservesFromBiblionumber {
257 my ($biblionumber) = shift or return (0, []);
258 my ($all_dates) = shift;
259 my $dbh = C4::Context->dbh;
261 # Find the desired items in the reserves
262 my $query = "
263 SELECT branchcode,
264 timestamp AS rtimestamp,
265 priority,
266 biblionumber,
267 borrowernumber,
268 reservedate,
269 constrainttype,
270 found,
271 itemnumber,
272 reservenotes,
273 expirationdate,
274 lowestPriority,
275 suspend,
276 suspend_until
277 FROM reserves
278 WHERE biblionumber = ? ";
279 unless ( $all_dates ) {
280 $query .= "AND reservedate <= CURRENT_DATE()";
282 $query .= "ORDER BY priority";
283 my $sth = $dbh->prepare($query);
284 $sth->execute($biblionumber);
285 my @results;
286 my $i = 0;
287 while ( my $data = $sth->fetchrow_hashref ) {
289 # FIXME - What is this doing? How do constraints work?
290 if ($data->{constrainttype} eq 'o') {
291 $query = '
292 SELECT biblioitemnumber
293 FROM reserveconstraints
294 WHERE biblionumber = ?
295 AND borrowernumber = ?
296 AND reservedate = ?
298 my $csth = $dbh->prepare($query);
299 $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
300 my @bibitemno;
301 while ( my $bibitemnos = $csth->fetchrow_array ) {
302 push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref
304 my $count = scalar @bibitemno;
306 # if we have two or more different specific itemtypes
307 # reserved by same person on same day
308 my $bdata;
309 if ( $count > 1 ) {
310 $bdata = GetBiblioItemData( $bibitemno[$i] ); # FIXME: This doesn't make sense.
311 $i++; # $i can increase each pass, but the next @bibitemno might be smaller?
313 else {
314 # Look up the book we just found.
315 $bdata = GetBiblioItemData( $bibitemno[0] );
317 # Add the results of this latest search to the current
318 # results.
319 # FIXME - An 'each' would probably be more efficient.
320 foreach my $key ( keys %$bdata ) {
321 $data->{$key} = $bdata->{$key};
324 push @results, $data;
326 return ( $#results + 1, \@results );
329 =head2 GetReservesFromItemnumber
331 ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber);
333 TODO :: Description here
335 =cut
337 sub GetReservesFromItemnumber {
338 my ( $itemnumber, $all_dates ) = @_;
339 my $dbh = C4::Context->dbh;
340 my $query = "
341 SELECT reservedate,borrowernumber,branchcode
342 FROM reserves
343 WHERE itemnumber=?
345 unless ( $all_dates ) {
346 $query .= " AND reservedate <= CURRENT_DATE()";
348 my $sth_res = $dbh->prepare($query);
349 $sth_res->execute($itemnumber);
350 my ( $reservedate, $borrowernumber,$branchcode ) = $sth_res->fetchrow_array;
351 return ( $reservedate, $borrowernumber, $branchcode );
354 =head2 GetReservesFromBorrowernumber
356 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
358 TODO :: Descritpion
360 =cut
362 sub GetReservesFromBorrowernumber {
363 my ( $borrowernumber, $status ) = @_;
364 my $dbh = C4::Context->dbh;
365 my $sth;
366 if ($status) {
367 $sth = $dbh->prepare("
368 SELECT *
369 FROM reserves
370 WHERE borrowernumber=?
371 AND found =?
372 ORDER BY reservedate
374 $sth->execute($borrowernumber,$status);
375 } else {
376 $sth = $dbh->prepare("
377 SELECT *
378 FROM reserves
379 WHERE borrowernumber=?
380 ORDER BY reservedate
382 $sth->execute($borrowernumber);
384 my $data = $sth->fetchall_arrayref({});
385 return @$data;
387 #-------------------------------------------------------------------------------------
388 =head2 CanBookBeReserved
390 $error = &CanBookBeReserved($borrowernumber, $biblionumber)
392 =cut
394 sub CanBookBeReserved{
395 my ($borrowernumber, $biblionumber) = @_;
397 my $items = GetItemnumbersForBiblio($biblionumber);
398 #get items linked via host records
399 my @hostitems = get_hostitemnumbers_of($biblionumber);
400 if (@hostitems){
401 push (@$items,@hostitems);
404 foreach my $item (@$items){
405 return 1 if CanItemBeReserved($borrowernumber, $item);
407 return 0;
410 =head2 CanItemBeReserved
412 $error = &CanItemBeReserved($borrowernumber, $itemnumber)
414 This function return 1 if an item can be issued by this borrower.
416 =cut
418 sub CanItemBeReserved{
419 my ($borrowernumber, $itemnumber) = @_;
421 my $dbh = C4::Context->dbh;
422 my $allowedreserves = 0;
424 my $controlbranch = C4::Context->preference('ReservesControlBranch');
425 my $itype = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype";
427 # we retrieve borrowers and items informations #
428 my $item = GetItem($itemnumber);
429 my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
431 # we retrieve user rights on this itemtype and branchcode
432 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
433 FROM issuingrules
434 WHERE (categorycode in (?,'*') )
435 AND (itemtype IN (?,'*'))
436 AND (branchcode IN (?,'*'))
437 ORDER BY
438 categorycode DESC,
439 itemtype DESC,
440 branchcode DESC;"
443 my $querycount ="SELECT
444 count(*) as count
445 FROM reserves
446 LEFT JOIN items USING (itemnumber)
447 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
448 LEFT JOIN borrowers USING (borrowernumber)
449 WHERE borrowernumber = ?
453 my $itemtype = $item->{$itype};
454 my $categorycode = $borrower->{categorycode};
455 my $branchcode = "";
456 my $branchfield = "reserves.branchcode";
458 if( $controlbranch eq "ItemHomeLibrary" ){
459 $branchfield = "items.homebranch";
460 $branchcode = $item->{homebranch};
461 }elsif( $controlbranch eq "PatronLibrary" ){
462 $branchfield = "borrowers.branchcode";
463 $branchcode = $borrower->{branchcode};
466 # we retrieve rights
467 $sth->execute($categorycode, $itemtype, $branchcode);
468 if(my $rights = $sth->fetchrow_hashref()){
469 $itemtype = $rights->{itemtype};
470 $allowedreserves = $rights->{reservesallowed};
471 }else{
472 $itemtype = '*';
475 # we retrieve count
477 $querycount .= "AND $branchfield = ?";
479 $querycount .= " AND $itype = ?" if ($itemtype ne "*");
480 my $sthcount = $dbh->prepare($querycount);
482 if($itemtype eq "*"){
483 $sthcount->execute($borrowernumber, $branchcode);
484 }else{
485 $sthcount->execute($borrowernumber, $branchcode, $itemtype);
488 my $reservecount = "0";
489 if(my $rowcount = $sthcount->fetchrow_hashref()){
490 $reservecount = $rowcount->{count};
493 # we check if it's ok or not
494 if( $reservecount < $allowedreserves ){
495 return 1;
496 }else{
497 return 0;
500 #--------------------------------------------------------------------------------
501 =head2 GetReserveCount
503 $number = &GetReserveCount($borrowernumber);
505 this function returns the number of reservation for a borrower given on input arg.
507 =cut
509 sub GetReserveCount {
510 my ($borrowernumber) = @_;
512 my $dbh = C4::Context->dbh;
514 my $query = '
515 SELECT COUNT(*) AS counter
516 FROM reserves
517 WHERE borrowernumber = ?
519 my $sth = $dbh->prepare($query);
520 $sth->execute($borrowernumber);
521 my $row = $sth->fetchrow_hashref;
522 return $row->{counter};
525 =head2 GetOtherReserves
527 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
529 Check queued list of this document and check if this document must be transfered
531 =cut
533 sub GetOtherReserves {
534 my ($itemnumber) = @_;
535 my $messages;
536 my $nextreservinfo;
537 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
538 if ($checkreserves) {
539 my $iteminfo = GetItem($itemnumber);
540 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
541 $messages->{'transfert'} = $checkreserves->{'branchcode'};
542 #minus priorities of others reservs
543 ModReserveMinusPriority(
544 $itemnumber,
545 $checkreserves->{'borrowernumber'},
546 $iteminfo->{'biblionumber'}
549 #launch the subroutine dotransfer
550 C4::Items::ModItemTransfer(
551 $itemnumber,
552 $iteminfo->{'holdingbranch'},
553 $checkreserves->{'branchcode'}
558 #step 2b : case of a reservation on the same branch, set the waiting status
559 else {
560 $messages->{'waiting'} = 1;
561 ModReserveMinusPriority(
562 $itemnumber,
563 $checkreserves->{'borrowernumber'},
564 $iteminfo->{'biblionumber'}
566 ModReserveStatus($itemnumber,'W');
569 $nextreservinfo = $checkreserves->{'borrowernumber'};
572 return ( $messages, $nextreservinfo );
575 =head2 GetReserveFee
577 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
579 Calculate the fee for a reserve
581 =cut
583 sub GetReserveFee {
584 my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
586 #check for issues;
587 my $dbh = C4::Context->dbh;
588 my $const = lc substr( $constraint, 0, 1 );
589 my $query = qq/
590 SELECT * FROM borrowers
591 LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
592 WHERE borrowernumber = ?
594 my $sth = $dbh->prepare($query);
595 $sth->execute($borrowernumber);
596 my $data = $sth->fetchrow_hashref;
597 $sth->finish();
598 my $fee = $data->{'reservefee'};
599 my $cntitems = @- > $bibitems;
601 if ( $fee > 0 ) {
603 # check for items on issue
604 # first find biblioitem records
605 my @biblioitems;
606 my $sth1 = $dbh->prepare(
607 "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
608 WHERE (biblio.biblionumber = ?)"
610 $sth1->execute($biblionumber);
611 while ( my $data1 = $sth1->fetchrow_hashref ) {
612 if ( $const eq "a" ) {
613 push @biblioitems, $data1;
615 else {
616 my $found = 0;
617 my $x = 0;
618 while ( $x < $cntitems ) {
619 if ( @$bibitems->{'biblioitemnumber'} ==
620 $data->{'biblioitemnumber'} )
622 $found = 1;
624 $x++;
626 if ( $const eq 'o' ) {
627 if ( $found == 1 ) {
628 push @biblioitems, $data1;
631 else {
632 if ( $found == 0 ) {
633 push @biblioitems, $data1;
638 $sth1->finish;
639 my $cntitemsfound = @biblioitems;
640 my $issues = 0;
641 my $x = 0;
642 my $allissued = 1;
643 while ( $x < $cntitemsfound ) {
644 my $bitdata = $biblioitems[$x];
645 my $sth2 = $dbh->prepare(
646 "SELECT * FROM items
647 WHERE biblioitemnumber = ?"
649 $sth2->execute( $bitdata->{'biblioitemnumber'} );
650 while ( my $itdata = $sth2->fetchrow_hashref ) {
651 my $sth3 = $dbh->prepare(
652 "SELECT * FROM issues
653 WHERE itemnumber = ?"
655 $sth3->execute( $itdata->{'itemnumber'} );
656 if ( my $isdata = $sth3->fetchrow_hashref ) {
658 else {
659 $allissued = 0;
662 $x++;
664 if ( $allissued == 0 ) {
665 my $rsth =
666 $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
667 $rsth->execute($biblionumber);
668 if ( my $rdata = $rsth->fetchrow_hashref ) {
670 else {
671 $fee = 0;
675 return $fee;
678 =head2 GetReservesToBranch
680 @transreserv = GetReservesToBranch( $frombranch );
682 Get reserve list for a given branch
684 =cut
686 sub GetReservesToBranch {
687 my ( $frombranch ) = @_;
688 my $dbh = C4::Context->dbh;
689 my $sth = $dbh->prepare(
690 "SELECT borrowernumber,reservedate,itemnumber,timestamp
691 FROM reserves
692 WHERE priority='0'
693 AND branchcode=?"
695 $sth->execute( $frombranch );
696 my @transreserv;
697 my $i = 0;
698 while ( my $data = $sth->fetchrow_hashref ) {
699 $transreserv[$i] = $data;
700 $i++;
702 return (@transreserv);
705 =head2 GetReservesForBranch
707 @transreserv = GetReservesForBranch($frombranch);
709 =cut
711 sub GetReservesForBranch {
712 my ($frombranch) = @_;
713 my $dbh = C4::Context->dbh;
714 my $query = "SELECT borrowernumber,reservedate,itemnumber,waitingdate
715 FROM reserves
716 WHERE priority='0'
717 AND found='W' ";
718 if ($frombranch){
719 $query .= " AND branchcode=? ";
721 $query .= "ORDER BY waitingdate" ;
722 my $sth = $dbh->prepare($query);
723 if ($frombranch){
724 $sth->execute($frombranch);
726 else {
727 $sth->execute();
729 my @transreserv;
730 my $i = 0;
731 while ( my $data = $sth->fetchrow_hashref ) {
732 $transreserv[$i] = $data;
733 $i++;
735 return (@transreserv);
738 =head2 GetReserveStatus
740 $reservestatus = GetReserveStatus($itemnumber, $biblionumber);
742 Take an itemnumber or a biblionumber and return the status of the reserve places on it.
743 If several reserves exist, the reserve with the lower priority is given.
745 =cut
747 sub GetReserveStatus {
748 my ($itemnumber, $biblionumber) = @_;
750 my $dbh = C4::Context->dbh;
752 my ($sth, $found, $priority);
753 if ( $itemnumber ) {
754 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
755 $sth->execute($itemnumber);
756 ($found, $priority) = $sth->fetchrow_array;
759 if ( $biblionumber and not defined $found and not defined $priority ) {
760 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE biblionumber = ? order by priority LIMIT 1");
761 $sth->execute($biblionumber);
762 ($found, $priority) = $sth->fetchrow_array;
765 if(defined $found) {
766 return 'Waiting' if $found eq 'W' and $priority == 0;
767 return 'Finished' if $found eq 'F';
768 return 'Reserved' if $priority > 0;
770 return '';
771 #empty string here will remove need for checking undef, or less log lines
774 =head2 CheckReserves
776 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
777 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
779 Find a book in the reserves.
781 C<$itemnumber> is the book's item number.
783 As I understand it, C<&CheckReserves> looks for the given item in the
784 reserves. If it is found, that's a match, and C<$status> is set to
785 C<Waiting>.
787 Otherwise, it finds the most important item in the reserves with the
788 same biblio number as this book (I'm not clear on this) and returns it
789 with C<$status> set to C<Reserved>.
791 C<&CheckReserves> returns a two-element list:
793 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
795 C<$reserve> is the reserve item that matched. It is a
796 reference-to-hash whose keys are mostly the fields of the reserves
797 table in the Koha database.
799 =cut
801 sub CheckReserves {
802 my ( $item, $barcode ) = @_;
803 my $dbh = C4::Context->dbh;
804 my $sth;
805 my $select;
806 if (C4::Context->preference('item-level_itypes')){
807 $select = "
808 SELECT items.biblionumber,
809 items.biblioitemnumber,
810 itemtypes.notforloan,
811 items.notforloan AS itemnotforloan,
812 items.itemnumber
813 FROM items
814 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
815 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
818 else {
819 $select = "
820 SELECT items.biblionumber,
821 items.biblioitemnumber,
822 itemtypes.notforloan,
823 items.notforloan AS itemnotforloan,
824 items.itemnumber
825 FROM items
826 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
827 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
831 if ($item) {
832 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
833 $sth->execute($item);
835 else {
836 $sth = $dbh->prepare("$select WHERE barcode = ?");
837 $sth->execute($barcode);
839 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
840 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
842 return ( '' ) unless $itemnumber; # bail if we got nothing.
844 # if item is not for loan it cannot be reserved either.....
845 # execpt where items.notforloan < 0 : This indicates the item is holdable.
846 return ( '' ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
848 # Find this item in the reserves
849 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber );
851 # $priority and $highest are used to find the most important item
852 # in the list returned by &_Findgroupreserve. (The lower $priority,
853 # the more important the item.)
854 # $highest is the most important item we've seen so far.
855 my $highest;
856 if (scalar @reserves) {
857 my $priority = 10000000;
858 foreach my $res (@reserves) {
859 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
860 return ( "Waiting", $res, \@reserves ); # Found it
861 } else {
862 # See if this item is more important than what we've got so far
863 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
864 my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'});
865 my $iteminfo=C4::Items::GetItem($itemnumber);
866 my $branch=C4::Circulation::_GetCircControlBranch($iteminfo,$borrowerinfo);
867 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
868 next if ($branchitemrule->{'holdallowed'} == 0);
869 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
870 $priority = $res->{'priority'};
871 $highest = $res;
877 # If we get this far, then no exact match was found.
878 # We return the most important (i.e. next) reservation.
879 if ($highest) {
880 $highest->{'itemnumber'} = $item;
881 return ( "Reserved", $highest, \@reserves );
884 return ( '' );
887 =head2 CancelExpiredReserves
889 CancelExpiredReserves();
891 Cancels all reserves with an expiration date from before today.
893 =cut
895 sub CancelExpiredReserves {
897 # Cancel reserves that have passed their expiration date.
898 my $dbh = C4::Context->dbh;
899 my $sth = $dbh->prepare( "
900 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
901 AND expirationdate IS NOT NULL
902 AND found IS NULL
903 " );
904 $sth->execute();
906 while ( my $res = $sth->fetchrow_hashref() ) {
907 CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
910 # Cancel reserves that have been waiting too long
911 if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
912 my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
913 my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
915 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
916 $sth = $dbh->prepare( $query );
917 $sth->execute( $max_pickup_delay );
919 while (my $res = $sth->fetchrow_hashref ) {
920 if ( $charge ) {
921 manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
924 CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
930 =head2 AutoUnsuspendReserves
932 AutoUnsuspendReserves();
934 Unsuspends all suspended reserves with a suspend_until date from before today.
936 =cut
938 sub AutoUnsuspendReserves {
940 my $dbh = C4::Context->dbh;
942 my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
943 my $sth = $dbh->prepare( $query );
944 $sth->execute();
948 =head2 CancelReserve
950 &CancelReserve($biblionumber, $itemnumber, $borrowernumber);
952 Cancels a reserve.
954 Use either C<$biblionumber> or C<$itemnumber> to specify the item to
955 cancel, but not both: if both are given, C<&CancelReserve> uses
956 C<$itemnumber>.
958 C<$borrowernumber> is the borrower number of the patron on whose
959 behalf the book was reserved.
961 If C<$biblionumber> was given, C<&CancelReserve> also adjusts the
962 priorities of the other people who are waiting on the book.
964 =cut
966 sub CancelReserve {
967 my ( $biblio, $item, $borr ) = @_;
968 my $dbh = C4::Context->dbh;
969 if ( $item and $borr ) {
970 # removing a waiting reserve record....
971 # update the database...
972 my $query = "
973 UPDATE reserves
974 SET cancellationdate = now(),
975 found = Null,
976 priority = 0
977 WHERE itemnumber = ?
978 AND borrowernumber = ?
980 my $sth = $dbh->prepare($query);
981 $sth->execute( $item, $borr );
982 $sth->finish;
983 $query = "
984 INSERT INTO old_reserves
985 SELECT * FROM reserves
986 WHERE itemnumber = ?
987 AND borrowernumber = ?
989 $sth = $dbh->prepare($query);
990 $sth->execute( $item, $borr );
991 $query = "
992 DELETE FROM reserves
993 WHERE itemnumber = ?
994 AND borrowernumber = ?
996 $sth = $dbh->prepare($query);
997 $sth->execute( $item, $borr );
999 else {
1000 # removing a reserve record....
1001 # get the prioritiy on this record....
1002 my $priority;
1003 my $query = qq/
1004 SELECT priority FROM reserves
1005 WHERE biblionumber = ?
1006 AND borrowernumber = ?
1007 AND cancellationdate IS NULL
1008 AND itemnumber IS NULL
1010 my $sth = $dbh->prepare($query);
1011 $sth->execute( $biblio, $borr );
1012 ($priority) = $sth->fetchrow_array;
1013 $sth->finish;
1014 $query = qq/
1015 UPDATE reserves
1016 SET cancellationdate = now(),
1017 found = Null,
1018 priority = 0
1019 WHERE biblionumber = ?
1020 AND borrowernumber = ?
1023 # update the database, removing the record...
1024 $sth = $dbh->prepare($query);
1025 $sth->execute( $biblio, $borr );
1026 $sth->finish;
1028 $query = qq/
1029 INSERT INTO old_reserves
1030 SELECT * FROM reserves
1031 WHERE biblionumber = ?
1032 AND borrowernumber = ?
1034 $sth = $dbh->prepare($query);
1035 $sth->execute( $biblio, $borr );
1037 $query = qq/
1038 DELETE FROM reserves
1039 WHERE biblionumber = ?
1040 AND borrowernumber = ?
1042 $sth = $dbh->prepare($query);
1043 $sth->execute( $biblio, $borr );
1045 # now fix the priority on the others....
1046 _FixPriority( $biblio, $borr );
1050 =head2 ModReserve
1052 ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber])
1054 Change a hold request's priority or cancel it.
1056 C<$rank> specifies the effect of the change. If C<$rank>
1057 is 'W' or 'n', nothing happens. This corresponds to leaving a
1058 request alone when changing its priority in the holds queue
1059 for a bib.
1061 If C<$rank> is 'del', the hold request is cancelled.
1063 If C<$rank> is an integer greater than zero, the priority of
1064 the request is set to that value. Since priority != 0 means
1065 that the item is not waiting on the hold shelf, setting the
1066 priority to a non-zero value also sets the request's found
1067 status and waiting date to NULL.
1069 The optional C<$itemnumber> parameter is used only when
1070 C<$rank> is a non-zero integer; if supplied, the itemnumber
1071 of the hold request is set accordingly; if omitted, the itemnumber
1072 is cleared.
1074 B<FIXME:> Note that the forgoing can have the effect of causing
1075 item-level hold requests to turn into title-level requests. This
1076 will be fixed once reserves has separate columns for requested
1077 itemnumber and supplying itemnumber.
1079 =cut
1081 sub ModReserve {
1082 #subroutine to update a reserve
1083 my ( $rank, $biblio, $borrower, $branch , $itemnumber, $suspend_until) = @_;
1084 return if $rank eq "W";
1085 return if $rank eq "n";
1086 my $dbh = C4::Context->dbh;
1087 if ( $rank eq "del" ) {
1088 my $query = qq/
1089 UPDATE reserves
1090 SET cancellationdate=now()
1091 WHERE biblionumber = ?
1092 AND borrowernumber = ?
1094 my $sth = $dbh->prepare($query);
1095 $sth->execute( $biblio, $borrower );
1096 $sth->finish;
1097 $query = qq/
1098 INSERT INTO old_reserves
1099 SELECT *
1100 FROM reserves
1101 WHERE biblionumber = ?
1102 AND borrowernumber = ?
1104 $sth = $dbh->prepare($query);
1105 $sth->execute( $biblio, $borrower );
1106 $query = qq/
1107 DELETE FROM reserves
1108 WHERE biblionumber = ?
1109 AND borrowernumber = ?
1111 $sth = $dbh->prepare($query);
1112 $sth->execute( $biblio, $borrower );
1115 elsif ($rank =~ /^\d+/ and $rank > 0) {
1116 my $query = "
1117 UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1118 WHERE biblionumber = ?
1119 AND borrowernumber = ?
1121 my $sth = $dbh->prepare($query);
1122 $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower);
1123 $sth->finish;
1125 if ( defined( $suspend_until ) ) {
1126 if ( $suspend_until ) {
1127 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1128 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $suspend_until, $biblio, $borrower ) );
1129 } else {
1130 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $biblio, $borrower ) );
1134 _FixPriority( $biblio, $borrower, $rank);
1138 =head2 ModReserveFill
1140 &ModReserveFill($reserve);
1142 Fill a reserve. If I understand this correctly, this means that the
1143 reserved book has been found and given to the patron who reserved it.
1145 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1146 whose keys are fields from the reserves table in the Koha database.
1148 =cut
1150 sub ModReserveFill {
1151 my ($res) = @_;
1152 my $dbh = C4::Context->dbh;
1153 # fill in a reserve record....
1154 my $biblionumber = $res->{'biblionumber'};
1155 my $borrowernumber = $res->{'borrowernumber'};
1156 my $resdate = $res->{'reservedate'};
1158 # get the priority on this record....
1159 my $priority;
1160 my $query = "SELECT priority
1161 FROM reserves
1162 WHERE biblionumber = ?
1163 AND borrowernumber = ?
1164 AND reservedate = ?";
1165 my $sth = $dbh->prepare($query);
1166 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1167 ($priority) = $sth->fetchrow_array;
1168 $sth->finish;
1170 # update the database...
1171 $query = "UPDATE reserves
1172 SET found = 'F',
1173 priority = 0
1174 WHERE biblionumber = ?
1175 AND reservedate = ?
1176 AND borrowernumber = ?
1178 $sth = $dbh->prepare($query);
1179 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1180 $sth->finish;
1182 # move to old_reserves
1183 $query = "INSERT INTO old_reserves
1184 SELECT * FROM reserves
1185 WHERE biblionumber = ?
1186 AND reservedate = ?
1187 AND borrowernumber = ?
1189 $sth = $dbh->prepare($query);
1190 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1191 $query = "DELETE FROM reserves
1192 WHERE biblionumber = ?
1193 AND reservedate = ?
1194 AND borrowernumber = ?
1196 $sth = $dbh->prepare($query);
1197 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1199 # now fix the priority on the others (if the priority wasn't
1200 # already sorted!)....
1201 unless ( $priority == 0 ) {
1202 _FixPriority( $biblionumber, $borrowernumber );
1206 =head2 ModReserveStatus
1208 &ModReserveStatus($itemnumber, $newstatus);
1210 Update the reserve status for the active (priority=0) reserve.
1212 $itemnumber is the itemnumber the reserve is on
1214 $newstatus is the new status.
1216 =cut
1218 sub ModReserveStatus {
1220 #first : check if we have a reservation for this item .
1221 my ($itemnumber, $newstatus) = @_;
1222 my $dbh = C4::Context->dbh;
1224 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1225 my $sth_set = $dbh->prepare($query);
1226 $sth_set->execute( $newstatus, $itemnumber );
1228 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1229 CartToShelf( $itemnumber );
1233 =head2 ModReserveAffect
1235 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1237 This function affect an item and a status for a given reserve
1238 The itemnumber parameter is used to find the biblionumber.
1239 with the biblionumber & the borrowernumber, we can affect the itemnumber
1240 to the correct reserve.
1242 if $transferToDo is not set, then the status is set to "Waiting" as well.
1243 otherwise, a transfer is on the way, and the end of the transfer will
1244 take care of the waiting status
1246 =cut
1248 sub ModReserveAffect {
1249 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1250 my $dbh = C4::Context->dbh;
1252 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1253 # attached to $itemnumber
1254 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1255 $sth->execute($itemnumber);
1256 my ($biblionumber) = $sth->fetchrow;
1258 # get request - need to find out if item is already
1259 # waiting in order to not send duplicate hold filled notifications
1260 my $request = GetReserveInfo($borrowernumber, $biblionumber);
1261 my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1263 # If we affect a reserve that has to be transfered, don't set to Waiting
1264 my $query;
1265 if ($transferToDo) {
1266 $query = "
1267 UPDATE reserves
1268 SET priority = 0,
1269 itemnumber = ?,
1270 found = 'T'
1271 WHERE borrowernumber = ?
1272 AND biblionumber = ?
1275 else {
1276 # affect the reserve to Waiting as well.
1277 $query = "
1278 UPDATE reserves
1279 SET priority = 0,
1280 found = 'W',
1281 waitingdate = NOW(),
1282 itemnumber = ?
1283 WHERE borrowernumber = ?
1284 AND biblionumber = ?
1287 $sth = $dbh->prepare($query);
1288 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1289 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1291 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1292 CartToShelf( $itemnumber );
1295 return;
1298 =head2 ModReserveCancelAll
1300 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1302 function to cancel reserv,check other reserves, and transfer document if it's necessary
1304 =cut
1306 sub ModReserveCancelAll {
1307 my $messages;
1308 my $nextreservinfo;
1309 my ( $itemnumber, $borrowernumber ) = @_;
1311 #step 1 : cancel the reservation
1312 my $CancelReserve = CancelReserve( undef, $itemnumber, $borrowernumber );
1314 #step 2 launch the subroutine of the others reserves
1315 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1317 return ( $messages, $nextreservinfo );
1320 =head2 ModReserveMinusPriority
1322 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1324 Reduce the values of queuded list
1326 =cut
1328 sub ModReserveMinusPriority {
1329 my ( $itemnumber, $borrowernumber, $biblionumber ) = @_;
1331 #first step update the value of the first person on reserv
1332 my $dbh = C4::Context->dbh;
1333 my $query = "
1334 UPDATE reserves
1335 SET priority = 0 , itemnumber = ?
1336 WHERE borrowernumber=?
1337 AND biblionumber=?
1339 my $sth_upd = $dbh->prepare($query);
1340 $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber );
1341 # second step update all others reservs
1342 _FixPriority($biblionumber, $borrowernumber, '0');
1345 =head2 GetReserveInfo
1347 &GetReserveInfo($borrowernumber,$biblionumber);
1349 Get item and borrower details for a current hold.
1350 Current implementation this query should have a single result.
1352 =cut
1354 sub GetReserveInfo {
1355 my ( $borrowernumber, $biblionumber ) = @_;
1356 my $dbh = C4::Context->dbh;
1357 my $strsth="SELECT
1358 reservedate,
1359 reservenotes,
1360 reserves.borrowernumber,
1361 reserves.biblionumber,
1362 reserves.branchcode,
1363 reserves.waitingdate,
1364 notificationdate,
1365 reminderdate,
1366 priority,
1367 found,
1368 firstname,
1369 surname,
1370 phone,
1371 email,
1372 address,
1373 address2,
1374 cardnumber,
1375 city,
1376 zipcode,
1377 biblio.title,
1378 biblio.author,
1379 items.holdingbranch,
1380 items.itemcallnumber,
1381 items.itemnumber,
1382 items.location,
1383 barcode,
1384 notes
1385 FROM reserves
1386 LEFT JOIN items USING(itemnumber)
1387 LEFT JOIN borrowers USING(borrowernumber)
1388 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1389 WHERE
1390 reserves.borrowernumber=?
1391 AND reserves.biblionumber=?";
1392 my $sth = $dbh->prepare($strsth);
1393 $sth->execute($borrowernumber,$biblionumber);
1395 my $data = $sth->fetchrow_hashref;
1396 return $data;
1400 =head2 IsAvailableForItemLevelRequest
1402 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1404 Checks whether a given item record is available for an
1405 item-level hold request. An item is available if
1407 * it is not lost AND
1408 * it is not damaged AND
1409 * it is not withdrawn AND
1410 * does not have a not for loan value > 0
1412 Whether or not the item is currently on loan is
1413 also checked - if the AllowOnShelfHolds system preference
1414 is ON, an item can be requested even if it is currently
1415 on loan to somebody else. If the system preference
1416 is OFF, an item that is currently checked out cannot
1417 be the target of an item-level hold request.
1419 Note that IsAvailableForItemLevelRequest() does not
1420 check if the staff operator is authorized to place
1421 a request on the item - in particular,
1422 this routine does not check IndependentBranches
1423 and canreservefromotherbranches.
1425 =cut
1427 sub IsAvailableForItemLevelRequest {
1428 my $itemnumber = shift;
1430 my $item = GetItem($itemnumber);
1432 # must check the notforloan setting of the itemtype
1433 # FIXME - a lot of places in the code do this
1434 # or something similar - need to be
1435 # consolidated
1436 my $dbh = C4::Context->dbh;
1437 my $notforloan_query;
1438 if (C4::Context->preference('item-level_itypes')) {
1439 $notforloan_query = "SELECT itemtypes.notforloan
1440 FROM items
1441 JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1442 WHERE itemnumber = ?";
1443 } else {
1444 $notforloan_query = "SELECT itemtypes.notforloan
1445 FROM items
1446 JOIN biblioitems USING (biblioitemnumber)
1447 JOIN itemtypes USING (itemtype)
1448 WHERE itemnumber = ?";
1450 my $sth = $dbh->prepare($notforloan_query);
1451 $sth->execute($itemnumber);
1452 my $notforloan_per_itemtype = 0;
1453 if (my ($notforloan) = $sth->fetchrow_array) {
1454 $notforloan_per_itemtype = 1 if $notforloan;
1457 my $available_per_item = 1;
1458 $available_per_item = 0 if $item->{itemlost} or
1459 ( $item->{notforloan} > 0 ) or
1460 ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1461 $item->{wthdrawn} or
1462 $notforloan_per_itemtype;
1465 if (C4::Context->preference('AllowOnShelfHolds')) {
1466 return $available_per_item;
1467 } else {
1468 return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "Waiting"));
1472 =head2 AlterPriority
1474 AlterPriority( $where, $borrowernumber, $biblionumber, $reservedate );
1476 This function changes a reserve's priority up, down, to the top, or to the bottom.
1477 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1479 =cut
1481 sub AlterPriority {
1482 my ( $where, $borrowernumber, $biblionumber ) = @_;
1484 my $dbh = C4::Context->dbh;
1486 ## Find this reserve
1487 my $sth = $dbh->prepare('SELECT * FROM reserves WHERE biblionumber = ? AND borrowernumber = ? AND cancellationdate IS NULL');
1488 $sth->execute( $biblionumber, $borrowernumber );
1489 my $reserve = $sth->fetchrow_hashref();
1490 $sth->finish();
1492 if ( $where eq 'up' || $where eq 'down' ) {
1494 my $priority = $reserve->{'priority'};
1495 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1496 _FixPriority( $biblionumber, $borrowernumber, $priority )
1498 } elsif ( $where eq 'top' ) {
1500 _FixPriority( $biblionumber, $borrowernumber, '1' )
1502 } elsif ( $where eq 'bottom' ) {
1504 _FixPriority( $biblionumber, $borrowernumber, '999999' )
1509 =head2 ToggleLowestPriority
1511 ToggleLowestPriority( $borrowernumber, $biblionumber );
1513 This function sets the lowestPriority field to true if is false, and false if it is true.
1515 =cut
1517 sub ToggleLowestPriority {
1518 my ( $borrowernumber, $biblionumber ) = @_;
1520 my $dbh = C4::Context->dbh;
1522 my $sth = $dbh->prepare(
1523 "UPDATE reserves SET lowestPriority = NOT lowestPriority
1524 WHERE biblionumber = ?
1525 AND borrowernumber = ?"
1527 $sth->execute(
1528 $biblionumber,
1529 $borrowernumber,
1531 $sth->finish;
1533 _FixPriority( $biblionumber, $borrowernumber, '999999' );
1536 =head2 ToggleSuspend
1538 ToggleSuspend( $borrowernumber, $biblionumber );
1540 This function sets the suspend field to true if is false, and false if it is true.
1541 If the reserve is currently suspended with a suspend_until date, that date will
1542 be cleared when it is unsuspended.
1544 =cut
1546 sub ToggleSuspend {
1547 my ( $borrowernumber, $biblionumber, $suspend_until ) = @_;
1549 $suspend_until = output_pref( dt_from_string( $suspend_until ), 'iso' ) if ( $suspend_until );
1551 my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1553 my $dbh = C4::Context->dbh;
1555 my $sth = $dbh->prepare(
1556 "UPDATE reserves SET suspend = NOT suspend,
1557 suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1558 WHERE biblionumber = ?
1559 AND borrowernumber = ?
1562 my @params;
1563 push( @params, $suspend_until ) if ( $suspend_until );
1564 push( @params, $biblionumber );
1565 push( @params, $borrowernumber );
1567 $sth->execute( @params );
1568 $sth->finish;
1571 =head2 SuspendAll
1573 SuspendAll(
1574 borrowernumber => $borrowernumber,
1575 [ biblionumber => $biblionumber, ]
1576 [ suspend_until => $suspend_until, ]
1577 [ suspend => $suspend ]
1580 This function accepts a set of hash keys as its parameters.
1581 It requires either borrowernumber or biblionumber, or both.
1583 suspend_until is wholly optional.
1585 =cut
1587 sub SuspendAll {
1588 my %params = @_;
1590 my $borrowernumber = $params{'borrowernumber'} || undef;
1591 my $biblionumber = $params{'biblionumber'} || undef;
1592 my $suspend_until = $params{'suspend_until'} || undef;
1593 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1595 $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1597 return unless ( $borrowernumber || $biblionumber );
1599 my ( $query, $sth, $dbh, @query_params );
1601 $query = "UPDATE reserves SET suspend = ? ";
1602 push( @query_params, $suspend );
1603 if ( !$suspend ) {
1604 $query .= ", suspend_until = NULL ";
1605 } elsif ( $suspend_until ) {
1606 $query .= ", suspend_until = ? ";
1607 push( @query_params, $suspend_until );
1609 $query .= " WHERE ";
1610 if ( $borrowernumber ) {
1611 $query .= " borrowernumber = ? ";
1612 push( @query_params, $borrowernumber );
1614 $query .= " AND " if ( $borrowernumber && $biblionumber );
1615 if ( $biblionumber ) {
1616 $query .= " biblionumber = ? ";
1617 push( @query_params, $biblionumber );
1619 $query .= " AND found IS NULL ";
1621 $dbh = C4::Context->dbh;
1622 $sth = $dbh->prepare( $query );
1623 $sth->execute( @query_params );
1624 $sth->finish;
1628 =head2 _FixPriority
1630 &_FixPriority($biblio,$borrowernumber,$rank,$ignoreSetLowestRank);
1632 Only used internally (so don't export it)
1633 Changed how this functions works #
1634 Now just gets an array of reserves in the rank order and updates them with
1635 the array index (+1 as array starts from 0)
1636 and if $rank is supplied will splice item from the array and splice it back in again
1637 in new priority rank
1639 =cut
1641 sub _FixPriority {
1642 my ( $biblio, $borrowernumber, $rank, $ignoreSetLowestRank ) = @_;
1643 my $dbh = C4::Context->dbh;
1644 if ( $rank eq "del" ) {
1645 CancelReserve( $biblio, undef, $borrowernumber );
1647 if ( $rank eq "W" || $rank eq "0" ) {
1649 # make sure priority for waiting or in-transit items is 0
1650 my $query = qq/
1651 UPDATE reserves
1652 SET priority = 0
1653 WHERE biblionumber = ?
1654 AND borrowernumber = ?
1655 AND found IN ('W', 'T')
1657 my $sth = $dbh->prepare($query);
1658 $sth->execute( $biblio, $borrowernumber );
1660 my @priority;
1661 my @reservedates;
1663 # get whats left
1664 # FIXME adding a new security in returned elements for changing priority,
1665 # now, we don't care anymore any reservations with itemnumber linked (suppose a waiting reserve)
1666 # This is wrong a waiting reserve has W set
1667 # The assumption that having an itemnumber set means waiting is wrong and should be corrected any place it occurs
1668 my $query = qq/
1669 SELECT borrowernumber, reservedate, constrainttype
1670 FROM reserves
1671 WHERE biblionumber = ?
1672 AND ((found <> 'W' AND found <> 'T') or found is NULL)
1673 ORDER BY priority ASC
1675 my $sth = $dbh->prepare($query);
1676 $sth->execute($biblio);
1677 while ( my $line = $sth->fetchrow_hashref ) {
1678 push( @reservedates, $line );
1679 push( @priority, $line );
1682 # To find the matching index
1683 my $i;
1684 my $key = -1; # to allow for 0 to be a valid result
1685 for ( $i = 0 ; $i < @priority ; $i++ ) {
1686 if ( $borrowernumber == $priority[$i]->{'borrowernumber'} ) {
1687 $key = $i; # save the index
1688 last;
1692 # if index exists in array then move it to new position
1693 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1694 my $new_rank = $rank -
1695 1; # $new_rank is what you want the new index to be in the array
1696 my $moving_item = splice( @priority, $key, 1 );
1697 splice( @priority, $new_rank, 0, $moving_item );
1700 # now fix the priority on those that are left....
1701 $query = "
1702 UPDATE reserves
1703 SET priority = ?
1704 WHERE biblionumber = ?
1705 AND borrowernumber = ?
1706 AND reservedate = ?
1707 AND found IS NULL
1709 $sth = $dbh->prepare($query);
1710 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1711 $sth->execute(
1712 $j + 1, $biblio,
1713 $priority[$j]->{'borrowernumber'},
1714 $priority[$j]->{'reservedate'}
1716 $sth->finish;
1719 $sth = $dbh->prepare( "SELECT borrowernumber FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1720 $sth->execute();
1722 unless ( $ignoreSetLowestRank ) {
1723 while ( my $res = $sth->fetchrow_hashref() ) {
1724 _FixPriority( $biblio, $res->{'borrowernumber'}, '999999', 1 );
1729 =head2 _Findgroupreserve
1731 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1733 Looks for an item-specific match first, then for a title-level match, returning the
1734 first match found. If neither, then we look for a 3rd kind of match based on
1735 reserve constraints.
1737 TODO: add more explanation about reserve constraints
1739 C<&_Findgroupreserve> returns :
1740 C<@results> is an array of references-to-hash whose keys are mostly
1741 fields from the reserves table of the Koha database, plus
1742 C<biblioitemnumber>.
1744 =cut
1746 sub _Findgroupreserve {
1747 my ( $bibitem, $biblio, $itemnumber ) = @_;
1748 my $dbh = C4::Context->dbh;
1750 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1751 # check for exact targetted match
1752 my $item_level_target_query = qq/
1753 SELECT reserves.biblionumber AS biblionumber,
1754 reserves.borrowernumber AS borrowernumber,
1755 reserves.reservedate AS reservedate,
1756 reserves.branchcode AS branchcode,
1757 reserves.cancellationdate AS cancellationdate,
1758 reserves.found AS found,
1759 reserves.reservenotes AS reservenotes,
1760 reserves.priority AS priority,
1761 reserves.timestamp AS timestamp,
1762 biblioitems.biblioitemnumber AS biblioitemnumber,
1763 reserves.itemnumber AS itemnumber
1764 FROM reserves
1765 JOIN biblioitems USING (biblionumber)
1766 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1767 WHERE found IS NULL
1768 AND priority > 0
1769 AND item_level_request = 1
1770 AND itemnumber = ?
1771 AND reservedate <= CURRENT_DATE()
1772 AND suspend = 0
1774 my $sth = $dbh->prepare($item_level_target_query);
1775 $sth->execute($itemnumber);
1776 my @results;
1777 if ( my $data = $sth->fetchrow_hashref ) {
1778 push( @results, $data );
1780 return @results if @results;
1782 # check for title-level targetted match
1783 my $title_level_target_query = qq/
1784 SELECT reserves.biblionumber AS biblionumber,
1785 reserves.borrowernumber AS borrowernumber,
1786 reserves.reservedate AS reservedate,
1787 reserves.branchcode AS branchcode,
1788 reserves.cancellationdate AS cancellationdate,
1789 reserves.found AS found,
1790 reserves.reservenotes AS reservenotes,
1791 reserves.priority AS priority,
1792 reserves.timestamp AS timestamp,
1793 biblioitems.biblioitemnumber AS biblioitemnumber,
1794 reserves.itemnumber AS itemnumber
1795 FROM reserves
1796 JOIN biblioitems USING (biblionumber)
1797 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1798 WHERE found IS NULL
1799 AND priority > 0
1800 AND item_level_request = 0
1801 AND hold_fill_targets.itemnumber = ?
1802 AND reservedate <= CURRENT_DATE()
1803 AND suspend = 0
1805 $sth = $dbh->prepare($title_level_target_query);
1806 $sth->execute($itemnumber);
1807 @results = ();
1808 if ( my $data = $sth->fetchrow_hashref ) {
1809 push( @results, $data );
1811 return @results if @results;
1813 my $query = qq/
1814 SELECT reserves.biblionumber AS biblionumber,
1815 reserves.borrowernumber AS borrowernumber,
1816 reserves.reservedate AS reservedate,
1817 reserves.waitingdate AS waitingdate,
1818 reserves.branchcode AS branchcode,
1819 reserves.cancellationdate AS cancellationdate,
1820 reserves.found AS found,
1821 reserves.reservenotes AS reservenotes,
1822 reserves.priority AS priority,
1823 reserves.timestamp AS timestamp,
1824 reserveconstraints.biblioitemnumber AS biblioitemnumber,
1825 reserves.itemnumber AS itemnumber
1826 FROM reserves
1827 LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1828 WHERE reserves.biblionumber = ?
1829 AND ( ( reserveconstraints.biblioitemnumber = ?
1830 AND reserves.borrowernumber = reserveconstraints.borrowernumber
1831 AND reserves.reservedate = reserveconstraints.reservedate )
1832 OR reserves.constrainttype='a' )
1833 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1834 AND reserves.reservedate <= CURRENT_DATE()
1835 AND suspend = 0
1837 $sth = $dbh->prepare($query);
1838 $sth->execute( $biblio, $bibitem, $itemnumber );
1839 @results = ();
1840 while ( my $data = $sth->fetchrow_hashref ) {
1841 push( @results, $data );
1843 return @results;
1846 =head2 _koha_notify_reserve
1848 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1850 Sends a notification to the patron that their hold has been filled (through
1851 ModReserveAffect, _not_ ModReserveFill)
1853 =cut
1855 sub _koha_notify_reserve {
1856 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1858 my $dbh = C4::Context->dbh;
1859 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1861 # Try to get the borrower's email address
1862 my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1864 my $letter_code;
1865 my $print_mode = 0;
1866 my $messagingprefs;
1867 if ( $to_address || $borrower->{'smsalertnumber'} ) {
1868 $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } );
1869 } else {
1870 $print_mode = 1;
1873 my $sth = $dbh->prepare("
1874 SELECT *
1875 FROM reserves
1876 WHERE borrowernumber = ?
1877 AND biblionumber = ?
1879 $sth->execute( $borrowernumber, $biblionumber );
1880 my $reserve = $sth->fetchrow_hashref;
1881 my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1883 my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1885 my %letter_params = (
1886 module => 'reserves',
1887 branchcode => $reserve->{branchcode},
1888 tables => {
1889 'branches' => $branch_details,
1890 'borrowers' => $borrower,
1891 'biblio' => $biblionumber,
1892 'reserves' => $reserve,
1893 'items', $reserve->{'itemnumber'},
1895 substitute => { today => C4::Dates->new()->output() },
1899 if ( $print_mode ) {
1900 $letter_params{ 'letter_code' } = 'HOLD_PRINT';
1901 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1903 C4::Letters::EnqueueLetter( {
1904 letter => $letter,
1905 borrowernumber => $borrowernumber,
1906 message_transport_type => 'print',
1907 } );
1909 return;
1912 if ( $to_address && defined $messagingprefs->{transports}->{'email'} ) {
1913 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'email'};
1914 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1916 C4::Letters::EnqueueLetter(
1917 { letter => $letter,
1918 borrowernumber => $borrowernumber,
1919 message_transport_type => 'email',
1920 from_address => $admin_email_address,
1925 if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports}->{'sms'} ) {
1926 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'sms'};
1927 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1929 C4::Letters::EnqueueLetter(
1930 { letter => $letter,
1931 borrowernumber => $borrowernumber,
1932 message_transport_type => 'sms',
1938 =head2 _ShiftPriorityByDateAndPriority
1940 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1942 This increments the priority of all reserves after the one
1943 with either the lowest date after C<$reservedate>
1944 or the lowest priority after C<$priority>.
1946 It effectively makes room for a new reserve to be inserted with a certain
1947 priority, which is returned.
1949 This is most useful when the reservedate can be set by the user. It allows
1950 the new reserve to be placed before other reserves that have a later
1951 reservedate. Since priority also is set by the form in reserves/request.pl
1952 the sub accounts for that too.
1954 =cut
1956 sub _ShiftPriorityByDateAndPriority {
1957 my ( $biblio, $resdate, $new_priority ) = @_;
1959 my $dbh = C4::Context->dbh;
1960 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1961 my $sth = $dbh->prepare( $query );
1962 $sth->execute( $biblio, $resdate, $new_priority );
1963 my $min_priority = $sth->fetchrow;
1964 # if no such matches are found, $new_priority remains as original value
1965 $new_priority = $min_priority if ( $min_priority );
1967 # Shift the priority up by one; works in conjunction with the next SQL statement
1968 $query = "UPDATE reserves
1969 SET priority = priority+1
1970 WHERE biblionumber = ?
1971 AND borrowernumber = ?
1972 AND reservedate = ?
1973 AND found IS NULL";
1974 my $sth_update = $dbh->prepare( $query );
1976 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1977 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1978 $sth = $dbh->prepare( $query );
1979 $sth->execute( $new_priority, $biblio );
1980 while ( my $row = $sth->fetchrow_hashref ) {
1981 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1984 return $new_priority; # so the caller knows what priority they wind up receiving
1987 =head2 MoveReserve
1989 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1991 Use when checking out an item to handle reserves
1992 If $cancelreserve boolean is set to true, it will remove existing reserve
1994 =cut
1996 sub MoveReserve {
1997 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1999 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
2000 return unless $res;
2002 my $biblionumber = $res->{biblionumber};
2003 my $biblioitemnumber = $res->{biblioitemnumber};
2005 if ($res->{borrowernumber} == $borrowernumber) {
2006 ModReserveFill($res);
2008 else {
2009 # warn "Reserved";
2010 # The item is reserved by someone else.
2011 # Find this item in the reserves
2013 my $borr_res;
2014 foreach (@$all_reserves) {
2015 $_->{'borrowernumber'} == $borrowernumber or next;
2016 $_->{'biblionumber'} == $biblionumber or next;
2018 $borr_res = $_;
2019 last;
2022 if ( $borr_res ) {
2023 # The item is reserved by the current patron
2024 ModReserveFill($borr_res);
2027 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2028 RevertWaitingStatus({ itemnumber => $itemnumber });
2030 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2031 CancelReserve(0, $res->{'itemnumber'}, $res->{'borrowernumber'});
2032 CancelReserve($res->{'biblionumber'}, 0, $res->{'borrowernumber'});
2037 =head2 MergeHolds
2039 MergeHolds($dbh,$to_biblio, $from_biblio);
2041 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2043 =cut
2045 sub MergeHolds {
2046 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2047 my $sth = $dbh->prepare(
2048 "SELECT count(*) as reservenumber FROM reserves WHERE biblionumber = ?"
2050 $sth->execute($from_biblio);
2051 if ( my $data = $sth->fetchrow_hashref() ) {
2053 # holds exist on old record, if not we don't need to do anything
2054 $sth = $dbh->prepare(
2055 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2056 $sth->execute( $to_biblio, $from_biblio );
2058 # Reorder by date
2059 # don't reorder those already waiting
2061 $sth = $dbh->prepare(
2062 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2064 my $upd_sth = $dbh->prepare(
2065 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2066 AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2068 $sth->execute( $to_biblio, 'W', 'T' );
2069 my $priority = 1;
2070 while ( my $reserve = $sth->fetchrow_hashref() ) {
2071 $upd_sth->execute(
2072 $priority, $to_biblio,
2073 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2074 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2076 $priority++;
2081 =head2 RevertWaitingStatus
2083 $success = RevertWaitingStatus({ itemnumber => $itemnumber });
2085 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2087 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2088 item level hold, even if it was only a bibliolevel hold to
2089 begin with. This is because we can no longer know if a hold
2090 was item-level or bib-level after a hold has been set to
2091 waiting status.
2093 =cut
2095 sub RevertWaitingStatus {
2096 my ( $params ) = @_;
2097 my $itemnumber = $params->{'itemnumber'};
2099 return unless ( $itemnumber );
2101 my $dbh = C4::Context->dbh;
2103 ## Get the waiting reserve we want to revert
2104 my $query = "
2105 SELECT * FROM reserves
2106 WHERE itemnumber = ?
2107 AND found IS NOT NULL
2109 my $sth = $dbh->prepare( $query );
2110 $sth->execute( $itemnumber );
2111 my $reserve = $sth->fetchrow_hashref();
2113 ## Increment the priority of all other non-waiting
2114 ## reserves for this bib record
2115 $query = "
2116 UPDATE reserves
2118 priority = priority + 1
2119 WHERE
2120 biblionumber = ?
2122 priority > 0
2124 $sth = $dbh->prepare( $query );
2125 $sth->execute( $reserve->{'biblionumber'} );
2127 ## Fix up the currently waiting reserve
2128 $query = "
2129 UPDATE reserves
2131 priority = 1,
2132 found = NULL,
2133 waitingdate = NULL
2134 WHERE
2135 reserve_id = ?
2137 $sth = $dbh->prepare( $query );
2138 return $sth->execute( $reserve->{'reserve_id'} );
2141 =head2 ReserveSlip
2143 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2145 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2147 =cut
2149 sub ReserveSlip {
2150 my ($branch, $borrowernumber, $biblionumber) = @_;
2152 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2154 my $reserve = GetReserveInfo($borrowernumber,$biblionumber )
2155 or return;
2157 return C4::Letters::GetPreparedLetter (
2158 module => 'circulation',
2159 letter_code => 'RESERVESLIP',
2160 branchcode => $branch,
2161 tables => {
2162 'reserves' => $reserve,
2163 'branches' => $reserve->{branchcode},
2164 'borrowers' => $reserve->{borrowernumber},
2165 'biblio' => $reserve->{biblionumber},
2166 'items' => $reserve->{itemnumber},
2171 =head1 AUTHOR
2173 Koha Development Team <http://koha-community.org/>
2175 =cut