Bug 9487: Allow item fields to be used in the HOLDPLACED notice
[koha.git] / C4 / Reserves.pm
blobd6812f1d99a71e7715ba9a246487d8b239d39797
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 &GetReserve
98 &GetReservesFromItemnumber
99 &GetReservesFromBiblionumber
100 &GetReservesFromBorrowernumber
101 &GetReservesForBranch
102 &GetReservesToBranch
103 &GetReserveCount
104 &GetReserveFee
105 &GetReserveInfo
106 &GetReserveStatus
108 &GetOtherReserves
110 &ModReserveFill
111 &ModReserveAffect
112 &ModReserve
113 &ModReserveStatus
114 &ModReserveCancelAll
115 &ModReserveMinusPriority
116 &MoveReserve
118 &CheckReserves
119 &CanBookBeReserved
120 &CanItemBeReserved
121 &CancelReserve
122 &CancelExpiredReserves
124 &AutoUnsuspendReserves
126 &IsAvailableForItemLevelRequest
128 &AlterPriority
129 &ToggleLowestPriority
131 &ReserveSlip
132 &ToggleSuspend
133 &SuspendAll
135 @EXPORT_OK = qw( MergeHolds );
138 =head2 AddReserve
140 AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
142 =cut
144 sub AddReserve {
145 my (
146 $branch, $borrowernumber, $biblionumber,
147 $constraint, $bibitems, $priority, $resdate, $expdate, $notes,
148 $title, $checkitem, $found
149 ) = @_;
150 my $fee =
151 GetReserveFee($borrowernumber, $biblionumber, $constraint,
152 $bibitems );
153 my $dbh = C4::Context->dbh;
154 my $const = lc substr( $constraint, 0, 1 );
155 $resdate = format_date_in_iso( $resdate ) if ( $resdate );
156 $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
157 if ($expdate) {
158 $expdate = format_date_in_iso( $expdate );
159 } else {
160 undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
162 if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
163 # Make room in reserves for this before those of a later reserve date
164 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
166 my $waitingdate;
168 # If the reserv had the waiting status, we had the value of the resdate
169 if ( $found eq 'W' ) {
170 $waitingdate = $resdate;
173 #eval {
174 # updates take place here
175 if ( $fee > 0 ) {
176 my $nextacctno = &getnextacctno( $borrowernumber );
177 my $query = qq/
178 INSERT INTO accountlines
179 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
180 VALUES
181 (?,?,now(),?,?,'Res',?)
183 my $usth = $dbh->prepare($query);
184 $usth->execute( $borrowernumber, $nextacctno, $fee,
185 "Reserve Charge - $title", $fee );
188 #if ($const eq 'a'){
189 my $query = qq/
190 INSERT INTO reserves
191 (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
192 priority,reservenotes,itemnumber,found,waitingdate,expirationdate)
193 VALUES
194 (?,?,?,?,?,
195 ?,?,?,?,?,?)
197 my $sth = $dbh->prepare($query);
198 $sth->execute(
199 $borrowernumber, $biblionumber, $resdate, $branch,
200 $const, $priority, $notes, $checkitem,
201 $found, $waitingdate, $expdate
204 # Send e-mail to librarian if syspref is active
205 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
206 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
207 my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode});
208 if ( my $letter = C4::Letters::GetPreparedLetter (
209 module => 'reserves',
210 letter_code => 'HOLDPLACED',
211 branchcode => $branch,
212 tables => {
213 'branches' => $branch_details,
214 'borrowers' => $borrower,
215 'biblio' => $biblionumber,
216 'items' => $checkitem,
218 ) ) {
220 my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
222 C4::Letters::EnqueueLetter(
223 { letter => $letter,
224 borrowernumber => $borrowernumber,
225 message_transport_type => 'email',
226 from_address => $admin_email_address,
227 to_address => $admin_email_address,
234 ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value?
235 $query = qq/
236 INSERT INTO reserveconstraints
237 (borrowernumber,biblionumber,reservedate,biblioitemnumber)
238 VALUES
239 (?,?,?,?)
241 $sth = $dbh->prepare($query); # keep prepare outside the loop!
242 foreach (@$bibitems) {
243 $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
246 return; # FIXME: why not have a useful return value?
249 =head2 GetReserve
251 $res = GetReserve( $reserve_id );
253 =cut
255 sub GetReserve {
256 my ($reserve_id) = @_;
258 my $dbh = C4::Context->dbh;
259 my $query = "SELECT * FROM reserves WHERE reserve_id = ?";
260 my $sth = $dbh->prepare( $query );
261 $sth->execute( $reserve_id );
262 my $res = $sth->fetchrow_hashref();
263 return $res;
266 =head2 GetReservesFromBiblionumber
268 ($count, $title_reserves) = GetReservesFromBiblionumber($biblionumber);
270 This function gets the list of reservations for one C<$biblionumber>, returning a count
271 of the reserves and an arrayref pointing to the reserves for C<$biblionumber>.
273 =cut
275 sub GetReservesFromBiblionumber {
276 my ($biblionumber) = shift or return (0, []);
277 my ($all_dates) = shift;
278 my $dbh = C4::Context->dbh;
280 # Find the desired items in the reserves
281 my $query = "
282 SELECT reserve_id,
283 branchcode,
284 timestamp AS rtimestamp,
285 priority,
286 biblionumber,
287 borrowernumber,
288 reservedate,
289 constrainttype,
290 found,
291 itemnumber,
292 reservenotes,
293 expirationdate,
294 lowestPriority,
295 suspend,
296 suspend_until
297 FROM reserves
298 WHERE biblionumber = ? ";
299 unless ( $all_dates ) {
300 $query .= "AND reservedate <= CURRENT_DATE()";
302 $query .= "ORDER BY priority";
303 my $sth = $dbh->prepare($query);
304 $sth->execute($biblionumber);
305 my @results;
306 my $i = 0;
307 while ( my $data = $sth->fetchrow_hashref ) {
309 # FIXME - What is this doing? How do constraints work?
310 if ($data->{constrainttype} eq 'o') {
311 $query = '
312 SELECT biblioitemnumber
313 FROM reserveconstraints
314 WHERE biblionumber = ?
315 AND borrowernumber = ?
316 AND reservedate = ?
318 my $csth = $dbh->prepare($query);
319 $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
320 my @bibitemno;
321 while ( my $bibitemnos = $csth->fetchrow_array ) {
322 push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref
324 my $count = scalar @bibitemno;
326 # if we have two or more different specific itemtypes
327 # reserved by same person on same day
328 my $bdata;
329 if ( $count > 1 ) {
330 $bdata = GetBiblioItemData( $bibitemno[$i] ); # FIXME: This doesn't make sense.
331 $i++; # $i can increase each pass, but the next @bibitemno might be smaller?
333 else {
334 # Look up the book we just found.
335 $bdata = GetBiblioItemData( $bibitemno[0] );
337 # Add the results of this latest search to the current
338 # results.
339 # FIXME - An 'each' would probably be more efficient.
340 foreach my $key ( keys %$bdata ) {
341 $data->{$key} = $bdata->{$key};
344 push @results, $data;
346 return ( $#results + 1, \@results );
349 =head2 GetReservesFromItemnumber
351 ( $reservedate, $borrowernumber, $branchcode, $reserve_id ) = GetReservesFromItemnumber($itemnumber);
353 TODO :: Description here
355 =cut
357 sub GetReservesFromItemnumber {
358 my ( $itemnumber, $all_dates ) = @_;
359 my $dbh = C4::Context->dbh;
360 my $query = "
361 SELECT reservedate,borrowernumber,branchcode,reserve_id
362 FROM reserves
363 WHERE itemnumber=?
365 unless ( $all_dates ) {
366 $query .= " AND reservedate <= CURRENT_DATE()";
368 my $sth_res = $dbh->prepare($query);
369 $sth_res->execute($itemnumber);
370 my ( $reservedate, $borrowernumber, $branchcode, $reserve_id ) = $sth_res->fetchrow_array;
371 return ( $reservedate, $borrowernumber, $branchcode, $reserve_id );
374 =head2 GetReservesFromBorrowernumber
376 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
378 TODO :: Descritpion
380 =cut
382 sub GetReservesFromBorrowernumber {
383 my ( $borrowernumber, $status ) = @_;
384 my $dbh = C4::Context->dbh;
385 my $sth;
386 if ($status) {
387 $sth = $dbh->prepare("
388 SELECT *
389 FROM reserves
390 WHERE borrowernumber=?
391 AND found =?
392 ORDER BY reservedate
394 $sth->execute($borrowernumber,$status);
395 } else {
396 $sth = $dbh->prepare("
397 SELECT *
398 FROM reserves
399 WHERE borrowernumber=?
400 ORDER BY reservedate
402 $sth->execute($borrowernumber);
404 my $data = $sth->fetchall_arrayref({});
405 return @$data;
407 #-------------------------------------------------------------------------------------
408 =head2 CanBookBeReserved
410 $error = &CanBookBeReserved($borrowernumber, $biblionumber)
412 =cut
414 sub CanBookBeReserved{
415 my ($borrowernumber, $biblionumber) = @_;
417 my $items = GetItemnumbersForBiblio($biblionumber);
418 #get items linked via host records
419 my @hostitems = get_hostitemnumbers_of($biblionumber);
420 if (@hostitems){
421 push (@$items,@hostitems);
424 foreach my $item (@$items){
425 return 1 if CanItemBeReserved($borrowernumber, $item);
427 return 0;
430 =head2 CanItemBeReserved
432 $error = &CanItemBeReserved($borrowernumber, $itemnumber)
434 This function return 1 if an item can be issued by this borrower.
436 =cut
438 sub CanItemBeReserved{
439 my ($borrowernumber, $itemnumber) = @_;
441 my $dbh = C4::Context->dbh;
442 my $allowedreserves = 0;
444 my $controlbranch = C4::Context->preference('ReservesControlBranch');
445 my $itype = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype";
447 # we retrieve borrowers and items informations #
448 my $item = GetItem($itemnumber);
449 my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
451 # we retrieve user rights on this itemtype and branchcode
452 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
453 FROM issuingrules
454 WHERE (categorycode in (?,'*') )
455 AND (itemtype IN (?,'*'))
456 AND (branchcode IN (?,'*'))
457 ORDER BY
458 categorycode DESC,
459 itemtype DESC,
460 branchcode DESC;"
463 my $querycount ="SELECT
464 count(*) as count
465 FROM reserves
466 LEFT JOIN items USING (itemnumber)
467 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
468 LEFT JOIN borrowers USING (borrowernumber)
469 WHERE borrowernumber = ?
473 my $itemtype = $item->{$itype};
474 my $categorycode = $borrower->{categorycode};
475 my $branchcode = "";
476 my $branchfield = "reserves.branchcode";
478 if( $controlbranch eq "ItemHomeLibrary" ){
479 $branchfield = "items.homebranch";
480 $branchcode = $item->{homebranch};
481 }elsif( $controlbranch eq "PatronLibrary" ){
482 $branchfield = "borrowers.branchcode";
483 $branchcode = $borrower->{branchcode};
486 # we retrieve rights
487 $sth->execute($categorycode, $itemtype, $branchcode);
488 if(my $rights = $sth->fetchrow_hashref()){
489 $itemtype = $rights->{itemtype};
490 $allowedreserves = $rights->{reservesallowed};
491 }else{
492 $itemtype = '*';
495 # we retrieve count
497 $querycount .= "AND $branchfield = ?";
499 $querycount .= " AND $itype = ?" if ($itemtype ne "*");
500 my $sthcount = $dbh->prepare($querycount);
502 if($itemtype eq "*"){
503 $sthcount->execute($borrowernumber, $branchcode);
504 }else{
505 $sthcount->execute($borrowernumber, $branchcode, $itemtype);
508 my $reservecount = "0";
509 if(my $rowcount = $sthcount->fetchrow_hashref()){
510 $reservecount = $rowcount->{count};
513 # we check if it's ok or not
514 if( $reservecount >= $allowedreserves ){
515 return 0;
518 # If reservecount is ok, we check item branch if IndependentBranches is ON
519 # and canreservefromotherbranches is OFF
520 if ( C4::Context->preference('IndependentBranches')
521 and !C4::Context->preference('canreservefromotherbranches') )
523 my $itembranch = $item->{homebranch};
524 if ($itembranch ne $borrower->{branchcode}) {
525 return 0;
529 return 1;
531 #--------------------------------------------------------------------------------
532 =head2 GetReserveCount
534 $number = &GetReserveCount($borrowernumber);
536 this function returns the number of reservation for a borrower given on input arg.
538 =cut
540 sub GetReserveCount {
541 my ($borrowernumber) = @_;
543 my $dbh = C4::Context->dbh;
545 my $query = "
546 SELECT COUNT(*) AS counter
547 FROM reserves
548 WHERE borrowernumber = ?
550 my $sth = $dbh->prepare($query);
551 $sth->execute($borrowernumber);
552 my $row = $sth->fetchrow_hashref;
553 return $row->{counter};
556 =head2 GetOtherReserves
558 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
560 Check queued list of this document and check if this document must be transfered
562 =cut
564 sub GetOtherReserves {
565 my ($itemnumber) = @_;
566 my $messages;
567 my $nextreservinfo;
568 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
569 if ($checkreserves) {
570 my $iteminfo = GetItem($itemnumber);
571 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
572 $messages->{'transfert'} = $checkreserves->{'branchcode'};
573 #minus priorities of others reservs
574 ModReserveMinusPriority(
575 $itemnumber,
576 $checkreserves->{'reserve_id'},
579 #launch the subroutine dotransfer
580 C4::Items::ModItemTransfer(
581 $itemnumber,
582 $iteminfo->{'holdingbranch'},
583 $checkreserves->{'branchcode'}
588 #step 2b : case of a reservation on the same branch, set the waiting status
589 else {
590 $messages->{'waiting'} = 1;
591 ModReserveMinusPriority(
592 $itemnumber,
593 $checkreserves->{'reserve_id'},
595 ModReserveStatus($itemnumber,'W');
598 $nextreservinfo = $checkreserves->{'borrowernumber'};
601 return ( $messages, $nextreservinfo );
604 =head2 GetReserveFee
606 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
608 Calculate the fee for a reserve
610 =cut
612 sub GetReserveFee {
613 my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
615 #check for issues;
616 my $dbh = C4::Context->dbh;
617 my $const = lc substr( $constraint, 0, 1 );
618 my $query = qq/
619 SELECT * FROM borrowers
620 LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
621 WHERE borrowernumber = ?
623 my $sth = $dbh->prepare($query);
624 $sth->execute($borrowernumber);
625 my $data = $sth->fetchrow_hashref;
626 $sth->finish();
627 my $fee = $data->{'reservefee'};
628 my $cntitems = @- > $bibitems;
630 if ( $fee > 0 ) {
632 # check for items on issue
633 # first find biblioitem records
634 my @biblioitems;
635 my $sth1 = $dbh->prepare(
636 "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
637 WHERE (biblio.biblionumber = ?)"
639 $sth1->execute($biblionumber);
640 while ( my $data1 = $sth1->fetchrow_hashref ) {
641 if ( $const eq "a" ) {
642 push @biblioitems, $data1;
644 else {
645 my $found = 0;
646 my $x = 0;
647 while ( $x < $cntitems ) {
648 if ( @$bibitems->{'biblioitemnumber'} ==
649 $data->{'biblioitemnumber'} )
651 $found = 1;
653 $x++;
655 if ( $const eq 'o' ) {
656 if ( $found == 1 ) {
657 push @biblioitems, $data1;
660 else {
661 if ( $found == 0 ) {
662 push @biblioitems, $data1;
667 $sth1->finish;
668 my $cntitemsfound = @biblioitems;
669 my $issues = 0;
670 my $x = 0;
671 my $allissued = 1;
672 while ( $x < $cntitemsfound ) {
673 my $bitdata = $biblioitems[$x];
674 my $sth2 = $dbh->prepare(
675 "SELECT * FROM items
676 WHERE biblioitemnumber = ?"
678 $sth2->execute( $bitdata->{'biblioitemnumber'} );
679 while ( my $itdata = $sth2->fetchrow_hashref ) {
680 my $sth3 = $dbh->prepare(
681 "SELECT * FROM issues
682 WHERE itemnumber = ?"
684 $sth3->execute( $itdata->{'itemnumber'} );
685 if ( my $isdata = $sth3->fetchrow_hashref ) {
687 else {
688 $allissued = 0;
691 $x++;
693 if ( $allissued == 0 ) {
694 my $rsth =
695 $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
696 $rsth->execute($biblionumber);
697 if ( my $rdata = $rsth->fetchrow_hashref ) {
699 else {
700 $fee = 0;
704 return $fee;
707 =head2 GetReservesToBranch
709 @transreserv = GetReservesToBranch( $frombranch );
711 Get reserve list for a given branch
713 =cut
715 sub GetReservesToBranch {
716 my ( $frombranch ) = @_;
717 my $dbh = C4::Context->dbh;
718 my $sth = $dbh->prepare(
719 "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
720 FROM reserves
721 WHERE priority='0'
722 AND branchcode=?"
724 $sth->execute( $frombranch );
725 my @transreserv;
726 my $i = 0;
727 while ( my $data = $sth->fetchrow_hashref ) {
728 $transreserv[$i] = $data;
729 $i++;
731 return (@transreserv);
734 =head2 GetReservesForBranch
736 @transreserv = GetReservesForBranch($frombranch);
738 =cut
740 sub GetReservesForBranch {
741 my ($frombranch) = @_;
742 my $dbh = C4::Context->dbh;
744 my $query = "
745 SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
746 FROM reserves
747 WHERE priority='0'
748 AND found='W'
750 $query .= " AND branchcode=? " if ( $frombranch );
751 $query .= "ORDER BY waitingdate" ;
753 my $sth = $dbh->prepare($query);
754 if ($frombranch){
755 $sth->execute($frombranch);
756 } else {
757 $sth->execute();
760 my @transreserv;
761 my $i = 0;
762 while ( my $data = $sth->fetchrow_hashref ) {
763 $transreserv[$i] = $data;
764 $i++;
766 return (@transreserv);
769 =head2 GetReserveStatus
771 $reservestatus = GetReserveStatus($itemnumber, $biblionumber);
773 Take an itemnumber or a biblionumber and return the status of the reserve places on it.
774 If several reserves exist, the reserve with the lower priority is given.
776 =cut
778 ## FIXME: I don't think this does what it thinks it does.
779 ## It only ever checks the first reserve result, even though
780 ## multiple reserves for that bib can have the itemnumber set
781 ## the sub is only used once in the codebase.
782 sub GetReserveStatus {
783 my ($itemnumber, $biblionumber) = @_;
785 my $dbh = C4::Context->dbh;
787 my ($sth, $found, $priority);
788 if ( $itemnumber ) {
789 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
790 $sth->execute($itemnumber);
791 ($found, $priority) = $sth->fetchrow_array;
794 if ( $biblionumber and not defined $found and not defined $priority ) {
795 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE biblionumber = ? order by priority LIMIT 1");
796 $sth->execute($biblionumber);
797 ($found, $priority) = $sth->fetchrow_array;
800 if(defined $found) {
801 return 'Waiting' if $found eq 'W' and $priority == 0;
802 return 'Finished' if $found eq 'F';
803 return 'Reserved' if $priority > 0;
805 return '';
806 #empty string here will remove need for checking undef, or less log lines
809 =head2 CheckReserves
811 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
812 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
814 Find a book in the reserves.
816 C<$itemnumber> is the book's item number.
818 As I understand it, C<&CheckReserves> looks for the given item in the
819 reserves. If it is found, that's a match, and C<$status> is set to
820 C<Waiting>.
822 Otherwise, it finds the most important item in the reserves with the
823 same biblio number as this book (I'm not clear on this) and returns it
824 with C<$status> set to C<Reserved>.
826 C<&CheckReserves> returns a two-element list:
828 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
830 C<$reserve> is the reserve item that matched. It is a
831 reference-to-hash whose keys are mostly the fields of the reserves
832 table in the Koha database.
834 =cut
836 sub CheckReserves {
837 my ( $item, $barcode ) = @_;
838 my $dbh = C4::Context->dbh;
839 my $sth;
840 my $select;
841 if (C4::Context->preference('item-level_itypes')){
842 $select = "
843 SELECT items.biblionumber,
844 items.biblioitemnumber,
845 itemtypes.notforloan,
846 items.notforloan AS itemnotforloan,
847 items.itemnumber
848 FROM items
849 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
850 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
853 else {
854 $select = "
855 SELECT items.biblionumber,
856 items.biblioitemnumber,
857 itemtypes.notforloan,
858 items.notforloan AS itemnotforloan,
859 items.itemnumber
860 FROM items
861 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
862 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
866 if ($item) {
867 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
868 $sth->execute($item);
870 else {
871 $sth = $dbh->prepare("$select WHERE barcode = ?");
872 $sth->execute($barcode);
874 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
875 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
877 return ( '' ) unless $itemnumber; # bail if we got nothing.
879 # if item is not for loan it cannot be reserved either.....
880 # execpt where items.notforloan < 0 : This indicates the item is holdable.
881 return ( '' ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
883 # Find this item in the reserves
884 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber );
886 # $priority and $highest are used to find the most important item
887 # in the list returned by &_Findgroupreserve. (The lower $priority,
888 # the more important the item.)
889 # $highest is the most important item we've seen so far.
890 my $highest;
891 if (scalar @reserves) {
892 my $priority = 10000000;
893 foreach my $res (@reserves) {
894 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
895 return ( "Waiting", $res, \@reserves ); # Found it
896 } else {
897 # See if this item is more important than what we've got so far
898 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
899 my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'});
900 my $iteminfo=C4::Items::GetItem($itemnumber);
901 my $branch=C4::Circulation::_GetCircControlBranch($iteminfo,$borrowerinfo);
902 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
903 next if ($branchitemrule->{'holdallowed'} == 0);
904 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
905 $priority = $res->{'priority'};
906 $highest = $res;
912 # If we get this far, then no exact match was found.
913 # We return the most important (i.e. next) reservation.
914 if ($highest) {
915 $highest->{'itemnumber'} = $item;
916 return ( "Reserved", $highest, \@reserves );
919 return ( '' );
922 =head2 CancelExpiredReserves
924 CancelExpiredReserves();
926 Cancels all reserves with an expiration date from before today.
928 =cut
930 sub CancelExpiredReserves {
932 # Cancel reserves that have passed their expiration date.
933 my $dbh = C4::Context->dbh;
934 my $sth = $dbh->prepare( "
935 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
936 AND expirationdate IS NOT NULL
937 AND found IS NULL
938 " );
939 $sth->execute();
941 while ( my $res = $sth->fetchrow_hashref() ) {
942 CancelReserve({ reserve_id => $res->{'reserve_id'} });
945 # Cancel reserves that have been waiting too long
946 if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
947 my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
948 my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
950 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
951 $sth = $dbh->prepare( $query );
952 $sth->execute( $max_pickup_delay );
954 while (my $res = $sth->fetchrow_hashref ) {
955 if ( $charge ) {
956 manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
959 CancelReserve({ reserve_id => $res->{'reserve_id'} });
965 =head2 AutoUnsuspendReserves
967 AutoUnsuspendReserves();
969 Unsuspends all suspended reserves with a suspend_until date from before today.
971 =cut
973 sub AutoUnsuspendReserves {
975 my $dbh = C4::Context->dbh;
977 my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
978 my $sth = $dbh->prepare( $query );
979 $sth->execute();
983 =head2 CancelReserve
985 CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber ] });
987 Cancels a reserve.
989 =cut
991 sub CancelReserve {
992 my ( $params ) = @_;
994 my $reserve_id = $params->{'reserve_id'};
995 $reserve_id = GetReserveId( $params ) unless ( $reserve_id );
997 return unless ( $reserve_id );
999 my $dbh = C4::Context->dbh;
1001 my $query = "
1002 UPDATE reserves
1003 SET cancellationdate = now(),
1004 found = Null,
1005 priority = 0
1006 WHERE reserve_id = ?
1008 my $sth = $dbh->prepare($query);
1009 $sth->execute( $reserve_id );
1010 $sth->finish;
1012 $query = "
1013 INSERT INTO old_reserves
1014 SELECT * FROM reserves
1015 WHERE reserve_id = ?
1017 $sth = $dbh->prepare($query);
1018 $sth->execute( $reserve_id );
1020 $query = "
1021 DELETE FROM reserves
1022 WHERE reserve_id = ?
1024 $sth = $dbh->prepare($query);
1025 $sth->execute( $reserve_id );
1027 # now fix the priority on the others....
1028 _FixPriority( $reserve_id );
1031 =head2 ModReserve
1033 ModReserve({ rank => $rank,
1034 reserve_id => $reserve_id,
1035 branchcode => $branchcode
1036 [, itemnumber => $itemnumber ]
1037 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1040 Change a hold request's priority or cancel it.
1042 C<$rank> specifies the effect of the change. If C<$rank>
1043 is 'W' or 'n', nothing happens. This corresponds to leaving a
1044 request alone when changing its priority in the holds queue
1045 for a bib.
1047 If C<$rank> is 'del', the hold request is cancelled.
1049 If C<$rank> is an integer greater than zero, the priority of
1050 the request is set to that value. Since priority != 0 means
1051 that the item is not waiting on the hold shelf, setting the
1052 priority to a non-zero value also sets the request's found
1053 status and waiting date to NULL.
1055 The optional C<$itemnumber> parameter is used only when
1056 C<$rank> is a non-zero integer; if supplied, the itemnumber
1057 of the hold request is set accordingly; if omitted, the itemnumber
1058 is cleared.
1060 B<FIXME:> Note that the forgoing can have the effect of causing
1061 item-level hold requests to turn into title-level requests. This
1062 will be fixed once reserves has separate columns for requested
1063 itemnumber and supplying itemnumber.
1065 =cut
1067 sub ModReserve {
1068 my ( $params ) = @_;
1070 my $rank = $params->{'rank'};
1071 my $reserve_id = $params->{'reserve_id'};
1072 my $branchcode = $params->{'branchcode'};
1073 my $itemnumber = $params->{'itemnumber'};
1074 my $suspend_until = $params->{'suspend_until'};
1075 my $borrowernumber = $params->{'borrowernumber'};
1076 my $biblionumber = $params->{'biblionumber'};
1078 return if $rank eq "W";
1079 return if $rank eq "n";
1081 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1082 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1084 my $dbh = C4::Context->dbh;
1085 if ( $rank eq "del" ) {
1086 my $query = "
1087 UPDATE reserves
1088 SET cancellationdate=now()
1089 WHERE reserve_id = ?
1091 my $sth = $dbh->prepare($query);
1092 $sth->execute( $reserve_id );
1093 $sth->finish;
1094 $query = "
1095 INSERT INTO old_reserves
1096 SELECT *
1097 FROM reserves
1098 WHERE reserve_id = ?
1100 $sth = $dbh->prepare($query);
1101 $sth->execute( $reserve_id );
1102 $query = "
1103 DELETE FROM reserves
1104 WHERE reserve_id = ?
1106 $sth = $dbh->prepare($query);
1107 $sth->execute( $reserve_id );
1110 elsif ($rank =~ /^\d+/ and $rank > 0) {
1111 my $query = "
1112 UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1113 WHERE reserve_id = ?
1115 my $sth = $dbh->prepare($query);
1116 $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
1117 $sth->finish;
1119 if ( defined( $suspend_until ) ) {
1120 if ( $suspend_until ) {
1121 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1122 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
1123 } else {
1124 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
1128 _FixPriority( $reserve_id, $rank );
1132 =head2 ModReserveFill
1134 &ModReserveFill($reserve);
1136 Fill a reserve. If I understand this correctly, this means that the
1137 reserved book has been found and given to the patron who reserved it.
1139 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1140 whose keys are fields from the reserves table in the Koha database.
1142 =cut
1144 sub ModReserveFill {
1145 my ($res) = @_;
1146 my $dbh = C4::Context->dbh;
1147 # fill in a reserve record....
1148 my $reserve_id = $res->{'reserve_id'};
1149 my $biblionumber = $res->{'biblionumber'};
1150 my $borrowernumber = $res->{'borrowernumber'};
1151 my $resdate = $res->{'reservedate'};
1153 # get the priority on this record....
1154 my $priority;
1155 my $query = "SELECT priority
1156 FROM reserves
1157 WHERE biblionumber = ?
1158 AND borrowernumber = ?
1159 AND reservedate = ?";
1160 my $sth = $dbh->prepare($query);
1161 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1162 ($priority) = $sth->fetchrow_array;
1163 $sth->finish;
1165 # update the database...
1166 $query = "UPDATE reserves
1167 SET found = 'F',
1168 priority = 0
1169 WHERE biblionumber = ?
1170 AND reservedate = ?
1171 AND borrowernumber = ?
1173 $sth = $dbh->prepare($query);
1174 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1175 $sth->finish;
1177 # move to old_reserves
1178 $query = "INSERT INTO old_reserves
1179 SELECT * FROM reserves
1180 WHERE biblionumber = ?
1181 AND reservedate = ?
1182 AND borrowernumber = ?
1184 $sth = $dbh->prepare($query);
1185 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1186 $query = "DELETE FROM reserves
1187 WHERE biblionumber = ?
1188 AND reservedate = ?
1189 AND borrowernumber = ?
1191 $sth = $dbh->prepare($query);
1192 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1194 # now fix the priority on the others (if the priority wasn't
1195 # already sorted!)....
1196 unless ( $priority == 0 ) {
1197 _FixPriority( $reserve_id );
1201 =head2 ModReserveStatus
1203 &ModReserveStatus($itemnumber, $newstatus);
1205 Update the reserve status for the active (priority=0) reserve.
1207 $itemnumber is the itemnumber the reserve is on
1209 $newstatus is the new status.
1211 =cut
1213 sub ModReserveStatus {
1215 #first : check if we have a reservation for this item .
1216 my ($itemnumber, $newstatus) = @_;
1217 my $dbh = C4::Context->dbh;
1219 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1220 my $sth_set = $dbh->prepare($query);
1221 $sth_set->execute( $newstatus, $itemnumber );
1223 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1224 CartToShelf( $itemnumber );
1228 =head2 ModReserveAffect
1230 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1232 This function affect an item and a status for a given reserve
1233 The itemnumber parameter is used to find the biblionumber.
1234 with the biblionumber & the borrowernumber, we can affect the itemnumber
1235 to the correct reserve.
1237 if $transferToDo is not set, then the status is set to "Waiting" as well.
1238 otherwise, a transfer is on the way, and the end of the transfer will
1239 take care of the waiting status
1241 =cut
1243 sub ModReserveAffect {
1244 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1245 my $dbh = C4::Context->dbh;
1247 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1248 # attached to $itemnumber
1249 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1250 $sth->execute($itemnumber);
1251 my ($biblionumber) = $sth->fetchrow;
1253 # get request - need to find out if item is already
1254 # waiting in order to not send duplicate hold filled notifications
1255 my $request = GetReserveInfo($borrowernumber, $biblionumber);
1256 my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1258 # If we affect a reserve that has to be transfered, don't set to Waiting
1259 my $query;
1260 if ($transferToDo) {
1261 $query = "
1262 UPDATE reserves
1263 SET priority = 0,
1264 itemnumber = ?,
1265 found = 'T'
1266 WHERE borrowernumber = ?
1267 AND biblionumber = ?
1270 else {
1271 # affect the reserve to Waiting as well.
1272 $query = "
1273 UPDATE reserves
1274 SET priority = 0,
1275 found = 'W',
1276 waitingdate = NOW(),
1277 itemnumber = ?
1278 WHERE borrowernumber = ?
1279 AND biblionumber = ?
1282 $sth = $dbh->prepare($query);
1283 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1284 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1286 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1287 CartToShelf( $itemnumber );
1290 return;
1293 =head2 ModReserveCancelAll
1295 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1297 function to cancel reserv,check other reserves, and transfer document if it's necessary
1299 =cut
1301 sub ModReserveCancelAll {
1302 my $messages;
1303 my $nextreservinfo;
1304 my ( $itemnumber, $borrowernumber ) = @_;
1306 #step 1 : cancel the reservation
1307 my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1309 #step 2 launch the subroutine of the others reserves
1310 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1312 return ( $messages, $nextreservinfo );
1315 =head2 ModReserveMinusPriority
1317 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1319 Reduce the values of queued list
1321 =cut
1323 sub ModReserveMinusPriority {
1324 my ( $itemnumber, $reserve_id ) = @_;
1326 #first step update the value of the first person on reserv
1327 my $dbh = C4::Context->dbh;
1328 my $query = "
1329 UPDATE reserves
1330 SET priority = 0 , itemnumber = ?
1331 WHERE reserve_id = ?
1333 my $sth_upd = $dbh->prepare($query);
1334 $sth_upd->execute( $itemnumber, $reserve_id );
1335 # second step update all others reservs
1336 _FixPriority( $reserve_id, '0');
1339 =head2 GetReserveInfo
1341 &GetReserveInfo($reserve_id);
1343 Get item and borrower details for a current hold.
1344 Current implementation this query should have a single result.
1346 =cut
1348 sub GetReserveInfo {
1349 my ( $reserve_id ) = @_;
1350 my $dbh = C4::Context->dbh;
1351 my $strsth="SELECT
1352 reserve_id,
1353 reservedate,
1354 reservenotes,
1355 reserves.borrowernumber,
1356 reserves.biblionumber,
1357 reserves.branchcode,
1358 reserves.waitingdate,
1359 notificationdate,
1360 reminderdate,
1361 priority,
1362 found,
1363 firstname,
1364 surname,
1365 phone,
1366 email,
1367 address,
1368 address2,
1369 cardnumber,
1370 city,
1371 zipcode,
1372 biblio.title,
1373 biblio.author,
1374 items.holdingbranch,
1375 items.itemcallnumber,
1376 items.itemnumber,
1377 items.location,
1378 barcode,
1379 notes
1380 FROM reserves
1381 LEFT JOIN items USING(itemnumber)
1382 LEFT JOIN borrowers USING(borrowernumber)
1383 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1384 WHERE reserves.reserve_id = ?";
1385 my $sth = $dbh->prepare($strsth);
1386 $sth->execute($reserve_id);
1388 my $data = $sth->fetchrow_hashref;
1389 return $data;
1392 =head2 IsAvailableForItemLevelRequest
1394 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1396 Checks whether a given item record is available for an
1397 item-level hold request. An item is available if
1399 * it is not lost AND
1400 * it is not damaged AND
1401 * it is not withdrawn AND
1402 * does not have a not for loan value > 0
1404 Whether or not the item is currently on loan is
1405 also checked - if the AllowOnShelfHolds system preference
1406 is ON, an item can be requested even if it is currently
1407 on loan to somebody else. If the system preference
1408 is OFF, an item that is currently checked out cannot
1409 be the target of an item-level hold request.
1411 Note that IsAvailableForItemLevelRequest() does not
1412 check if the staff operator is authorized to place
1413 a request on the item - in particular,
1414 this routine does not check IndependentBranches
1415 and canreservefromotherbranches.
1417 =cut
1419 sub IsAvailableForItemLevelRequest {
1420 my $itemnumber = shift;
1422 my $item = GetItem($itemnumber);
1424 # must check the notforloan setting of the itemtype
1425 # FIXME - a lot of places in the code do this
1426 # or something similar - need to be
1427 # consolidated
1428 my $dbh = C4::Context->dbh;
1429 my $notforloan_query;
1430 if (C4::Context->preference('item-level_itypes')) {
1431 $notforloan_query = "SELECT itemtypes.notforloan
1432 FROM items
1433 JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1434 WHERE itemnumber = ?";
1435 } else {
1436 $notforloan_query = "SELECT itemtypes.notforloan
1437 FROM items
1438 JOIN biblioitems USING (biblioitemnumber)
1439 JOIN itemtypes USING (itemtype)
1440 WHERE itemnumber = ?";
1442 my $sth = $dbh->prepare($notforloan_query);
1443 $sth->execute($itemnumber);
1444 my $notforloan_per_itemtype = 0;
1445 if (my ($notforloan) = $sth->fetchrow_array) {
1446 $notforloan_per_itemtype = 1 if $notforloan;
1449 my $available_per_item = 1;
1450 $available_per_item = 0 if $item->{itemlost} or
1451 ( $item->{notforloan} > 0 ) or
1452 ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1453 $item->{wthdrawn} or
1454 $notforloan_per_itemtype;
1457 if (C4::Context->preference('AllowOnShelfHolds')) {
1458 return $available_per_item;
1459 } else {
1460 return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "Waiting"));
1464 =head2 AlterPriority
1466 AlterPriority( $where, $reserve_id );
1468 This function changes a reserve's priority up, down, to the top, or to the bottom.
1469 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1471 =cut
1473 sub AlterPriority {
1474 my ( $where, $reserve_id ) = @_;
1476 my $dbh = C4::Context->dbh;
1478 my $reserve = GetReserve( $reserve_id );
1480 if ( $reserve->{cancellationdate} ) {
1481 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1482 return;
1485 if ( $where eq 'up' || $where eq 'down' ) {
1487 my $priority = $reserve->{'priority'};
1488 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1489 _FixPriority( $reserve_id, $priority )
1491 } elsif ( $where eq 'top' ) {
1493 _FixPriority( $reserve_id, '1' )
1495 } elsif ( $where eq 'bottom' ) {
1497 _FixPriority( $reserve_id, '999999' )
1502 =head2 ToggleLowestPriority
1504 ToggleLowestPriority( $borrowernumber, $biblionumber );
1506 This function sets the lowestPriority field to true if is false, and false if it is true.
1508 =cut
1510 sub ToggleLowestPriority {
1511 my ( $reserve_id ) = @_;
1513 my $dbh = C4::Context->dbh;
1515 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1516 $sth->execute( $reserve_id );
1517 $sth->finish;
1519 _FixPriority( $reserve_id, '999999' );
1522 =head2 ToggleSuspend
1524 ToggleSuspend( $reserve_id );
1526 This function sets the suspend field to true if is false, and false if it is true.
1527 If the reserve is currently suspended with a suspend_until date, that date will
1528 be cleared when it is unsuspended.
1530 =cut
1532 sub ToggleSuspend {
1533 my ( $reserve_id, $suspend_until ) = @_;
1535 $suspend_until = output_pref( dt_from_string( $suspend_until ), 'iso' ) if ( $suspend_until );
1537 my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1539 my $dbh = C4::Context->dbh;
1541 my $sth = $dbh->prepare(
1542 "UPDATE reserves SET suspend = NOT suspend,
1543 suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1544 WHERE reserve_id = ?
1547 my @params;
1548 push( @params, $suspend_until ) if ( $suspend_until );
1549 push( @params, $reserve_id );
1551 $sth->execute( @params );
1552 $sth->finish;
1555 =head2 SuspendAll
1557 SuspendAll(
1558 borrowernumber => $borrowernumber,
1559 [ biblionumber => $biblionumber, ]
1560 [ suspend_until => $suspend_until, ]
1561 [ suspend => $suspend ]
1564 This function accepts a set of hash keys as its parameters.
1565 It requires either borrowernumber or biblionumber, or both.
1567 suspend_until is wholly optional.
1569 =cut
1571 sub SuspendAll {
1572 my %params = @_;
1574 my $borrowernumber = $params{'borrowernumber'} || undef;
1575 my $biblionumber = $params{'biblionumber'} || undef;
1576 my $suspend_until = $params{'suspend_until'} || undef;
1577 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1579 $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1581 return unless ( $borrowernumber || $biblionumber );
1583 my ( $query, $sth, $dbh, @query_params );
1585 $query = "UPDATE reserves SET suspend = ? ";
1586 push( @query_params, $suspend );
1587 if ( !$suspend ) {
1588 $query .= ", suspend_until = NULL ";
1589 } elsif ( $suspend_until ) {
1590 $query .= ", suspend_until = ? ";
1591 push( @query_params, $suspend_until );
1593 $query .= " WHERE ";
1594 if ( $borrowernumber ) {
1595 $query .= " borrowernumber = ? ";
1596 push( @query_params, $borrowernumber );
1598 $query .= " AND " if ( $borrowernumber && $biblionumber );
1599 if ( $biblionumber ) {
1600 $query .= " biblionumber = ? ";
1601 push( @query_params, $biblionumber );
1603 $query .= " AND found IS NULL ";
1605 $dbh = C4::Context->dbh;
1606 $sth = $dbh->prepare( $query );
1607 $sth->execute( @query_params );
1608 $sth->finish;
1612 =head2 _FixPriority
1614 &_FixPriority( $reserve_id, $rank, $ignoreSetLowestRank);
1616 Only used internally (so don't export it)
1617 Changed how this functions works #
1618 Now just gets an array of reserves in the rank order and updates them with
1619 the array index (+1 as array starts from 0)
1620 and if $rank is supplied will splice item from the array and splice it back in again
1621 in new priority rank
1623 =cut
1625 sub _FixPriority {
1626 my ( $reserve_id, $rank, $ignoreSetLowestRank ) = @_;
1627 my $dbh = C4::Context->dbh;
1629 my $res = GetReserve( $reserve_id );
1631 if ( $rank eq "del" ) {
1632 CancelReserve({ reserve_id => $reserve_id });
1634 elsif ( $rank eq "W" || $rank eq "0" ) {
1636 # make sure priority for waiting or in-transit items is 0
1637 my $query = "
1638 UPDATE reserves
1639 SET priority = 0
1640 WHERE reserve_id = ?
1641 AND found IN ('W', 'T')
1643 my $sth = $dbh->prepare($query);
1644 $sth->execute( $reserve_id );
1646 my @priority;
1648 # get whats left
1649 my $query = "
1650 SELECT reserve_id, borrowernumber, reservedate, constrainttype
1651 FROM reserves
1652 WHERE biblionumber = ?
1653 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1654 ORDER BY priority ASC
1656 my $sth = $dbh->prepare($query);
1657 $sth->execute( $res->{'biblionumber'} );
1658 while ( my $line = $sth->fetchrow_hashref ) {
1659 push( @priority, $line );
1662 # To find the matching index
1663 my $i;
1664 my $key = -1; # to allow for 0 to be a valid result
1665 for ( $i = 0 ; $i < @priority ; $i++ ) {
1666 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1667 $key = $i; # save the index
1668 last;
1672 # if index exists in array then move it to new position
1673 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1674 my $new_rank = $rank -
1675 1; # $new_rank is what you want the new index to be in the array
1676 my $moving_item = splice( @priority, $key, 1 );
1677 splice( @priority, $new_rank, 0, $moving_item );
1680 # now fix the priority on those that are left....
1681 $query = "
1682 UPDATE reserves
1683 SET priority = ?
1684 WHERE reserve_id = ?
1686 $sth = $dbh->prepare($query);
1687 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1688 $sth->execute(
1689 $j + 1,
1690 $priority[$j]->{'reserve_id'}
1692 $sth->finish;
1695 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1696 $sth->execute();
1698 unless ( $ignoreSetLowestRank ) {
1699 while ( my $res = $sth->fetchrow_hashref() ) {
1700 _FixPriority( $res->{'reserve_id'}, '999999', 1 );
1705 =head2 _Findgroupreserve
1707 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1709 Looks for an item-specific match first, then for a title-level match, returning the
1710 first match found. If neither, then we look for a 3rd kind of match based on
1711 reserve constraints.
1713 TODO: add more explanation about reserve constraints
1715 C<&_Findgroupreserve> returns :
1716 C<@results> is an array of references-to-hash whose keys are mostly
1717 fields from the reserves table of the Koha database, plus
1718 C<biblioitemnumber>.
1720 =cut
1722 sub _Findgroupreserve {
1723 my ( $bibitem, $biblio, $itemnumber ) = @_;
1724 my $dbh = C4::Context->dbh;
1726 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1727 # check for exact targetted match
1728 my $item_level_target_query = qq/
1729 SELECT reserves.biblionumber AS biblionumber,
1730 reserves.borrowernumber AS borrowernumber,
1731 reserves.reservedate AS reservedate,
1732 reserves.branchcode AS branchcode,
1733 reserves.cancellationdate AS cancellationdate,
1734 reserves.found AS found,
1735 reserves.reservenotes AS reservenotes,
1736 reserves.priority AS priority,
1737 reserves.timestamp AS timestamp,
1738 biblioitems.biblioitemnumber AS biblioitemnumber,
1739 reserves.itemnumber AS itemnumber,
1740 reserves.reserve_id AS reserve_id
1741 FROM reserves
1742 JOIN biblioitems USING (biblionumber)
1743 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1744 WHERE found IS NULL
1745 AND priority > 0
1746 AND item_level_request = 1
1747 AND itemnumber = ?
1748 AND reservedate <= CURRENT_DATE()
1749 AND suspend = 0
1751 my $sth = $dbh->prepare($item_level_target_query);
1752 $sth->execute($itemnumber);
1753 my @results;
1754 if ( my $data = $sth->fetchrow_hashref ) {
1755 push( @results, $data );
1757 return @results if @results;
1759 # check for title-level targetted match
1760 my $title_level_target_query = qq/
1761 SELECT reserves.biblionumber AS biblionumber,
1762 reserves.borrowernumber AS borrowernumber,
1763 reserves.reservedate AS reservedate,
1764 reserves.branchcode AS branchcode,
1765 reserves.cancellationdate AS cancellationdate,
1766 reserves.found AS found,
1767 reserves.reservenotes AS reservenotes,
1768 reserves.priority AS priority,
1769 reserves.timestamp AS timestamp,
1770 biblioitems.biblioitemnumber AS biblioitemnumber,
1771 reserves.itemnumber AS itemnumber
1772 FROM reserves
1773 JOIN biblioitems USING (biblionumber)
1774 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1775 WHERE found IS NULL
1776 AND priority > 0
1777 AND item_level_request = 0
1778 AND hold_fill_targets.itemnumber = ?
1779 AND reservedate <= CURRENT_DATE()
1780 AND suspend = 0
1782 $sth = $dbh->prepare($title_level_target_query);
1783 $sth->execute($itemnumber);
1784 @results = ();
1785 if ( my $data = $sth->fetchrow_hashref ) {
1786 push( @results, $data );
1788 return @results if @results;
1790 my $query = qq/
1791 SELECT reserves.biblionumber AS biblionumber,
1792 reserves.borrowernumber AS borrowernumber,
1793 reserves.reservedate AS reservedate,
1794 reserves.waitingdate AS waitingdate,
1795 reserves.branchcode AS branchcode,
1796 reserves.cancellationdate AS cancellationdate,
1797 reserves.found AS found,
1798 reserves.reservenotes AS reservenotes,
1799 reserves.priority AS priority,
1800 reserves.timestamp AS timestamp,
1801 reserveconstraints.biblioitemnumber AS biblioitemnumber,
1802 reserves.itemnumber AS itemnumber
1803 FROM reserves
1804 LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1805 WHERE reserves.biblionumber = ?
1806 AND ( ( reserveconstraints.biblioitemnumber = ?
1807 AND reserves.borrowernumber = reserveconstraints.borrowernumber
1808 AND reserves.reservedate = reserveconstraints.reservedate )
1809 OR reserves.constrainttype='a' )
1810 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1811 AND reserves.reservedate <= CURRENT_DATE()
1812 AND suspend = 0
1814 $sth = $dbh->prepare($query);
1815 $sth->execute( $biblio, $bibitem, $itemnumber );
1816 @results = ();
1817 while ( my $data = $sth->fetchrow_hashref ) {
1818 push( @results, $data );
1820 return @results;
1823 =head2 _koha_notify_reserve
1825 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1827 Sends a notification to the patron that their hold has been filled (through
1828 ModReserveAffect, _not_ ModReserveFill)
1830 =cut
1832 sub _koha_notify_reserve {
1833 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1835 my $dbh = C4::Context->dbh;
1836 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1838 # Try to get the borrower's email address
1839 my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1841 my $letter_code;
1842 my $print_mode = 0;
1843 my $messagingprefs;
1844 if ( $to_address || $borrower->{'smsalertnumber'} ) {
1845 $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } );
1846 } else {
1847 $print_mode = 1;
1850 my $sth = $dbh->prepare("
1851 SELECT *
1852 FROM reserves
1853 WHERE borrowernumber = ?
1854 AND biblionumber = ?
1856 $sth->execute( $borrowernumber, $biblionumber );
1857 my $reserve = $sth->fetchrow_hashref;
1858 my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1860 my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1862 my %letter_params = (
1863 module => 'reserves',
1864 branchcode => $reserve->{branchcode},
1865 tables => {
1866 'branches' => $branch_details,
1867 'borrowers' => $borrower,
1868 'biblio' => $biblionumber,
1869 'reserves' => $reserve,
1870 'items', $reserve->{'itemnumber'},
1872 substitute => { today => C4::Dates->new()->output() },
1876 if ( $print_mode ) {
1877 $letter_params{ 'letter_code' } = 'HOLD_PRINT';
1878 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1880 C4::Letters::EnqueueLetter( {
1881 letter => $letter,
1882 borrowernumber => $borrowernumber,
1883 message_transport_type => 'print',
1884 } );
1886 return;
1889 if ( $to_address && defined $messagingprefs->{transports}->{'email'} ) {
1890 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'email'};
1891 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1893 C4::Letters::EnqueueLetter(
1894 { letter => $letter,
1895 borrowernumber => $borrowernumber,
1896 message_transport_type => 'email',
1897 from_address => $admin_email_address,
1902 if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports}->{'sms'} ) {
1903 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'sms'};
1904 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1906 C4::Letters::EnqueueLetter(
1907 { letter => $letter,
1908 borrowernumber => $borrowernumber,
1909 message_transport_type => 'sms',
1915 =head2 _ShiftPriorityByDateAndPriority
1917 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1919 This increments the priority of all reserves after the one
1920 with either the lowest date after C<$reservedate>
1921 or the lowest priority after C<$priority>.
1923 It effectively makes room for a new reserve to be inserted with a certain
1924 priority, which is returned.
1926 This is most useful when the reservedate can be set by the user. It allows
1927 the new reserve to be placed before other reserves that have a later
1928 reservedate. Since priority also is set by the form in reserves/request.pl
1929 the sub accounts for that too.
1931 =cut
1933 sub _ShiftPriorityByDateAndPriority {
1934 my ( $biblio, $resdate, $new_priority ) = @_;
1936 my $dbh = C4::Context->dbh;
1937 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1938 my $sth = $dbh->prepare( $query );
1939 $sth->execute( $biblio, $resdate, $new_priority );
1940 my $min_priority = $sth->fetchrow;
1941 # if no such matches are found, $new_priority remains as original value
1942 $new_priority = $min_priority if ( $min_priority );
1944 # Shift the priority up by one; works in conjunction with the next SQL statement
1945 $query = "UPDATE reserves
1946 SET priority = priority+1
1947 WHERE biblionumber = ?
1948 AND borrowernumber = ?
1949 AND reservedate = ?
1950 AND found IS NULL";
1951 my $sth_update = $dbh->prepare( $query );
1953 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1954 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1955 $sth = $dbh->prepare( $query );
1956 $sth->execute( $new_priority, $biblio );
1957 while ( my $row = $sth->fetchrow_hashref ) {
1958 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1961 return $new_priority; # so the caller knows what priority they wind up receiving
1964 =head2 MoveReserve
1966 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1968 Use when checking out an item to handle reserves
1969 If $cancelreserve boolean is set to true, it will remove existing reserve
1971 =cut
1973 sub MoveReserve {
1974 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1976 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
1977 return unless $res;
1979 my $biblionumber = $res->{biblionumber};
1980 my $biblioitemnumber = $res->{biblioitemnumber};
1982 if ($res->{borrowernumber} == $borrowernumber) {
1983 ModReserveFill($res);
1985 else {
1986 # warn "Reserved";
1987 # The item is reserved by someone else.
1988 # Find this item in the reserves
1990 my $borr_res;
1991 foreach (@$all_reserves) {
1992 $_->{'borrowernumber'} == $borrowernumber or next;
1993 $_->{'biblionumber'} == $biblionumber or next;
1995 $borr_res = $_;
1996 last;
1999 if ( $borr_res ) {
2000 # The item is reserved by the current patron
2001 ModReserveFill($borr_res);
2004 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2005 RevertWaitingStatus({ itemnumber => $itemnumber });
2007 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2008 CancelReserve({
2009 biblionumber => $res->{'biblionumber'},
2010 itemnumber => $res->{'itemnumber'},
2011 borrowernumber => $res->{'borrowernumber'}
2017 =head2 MergeHolds
2019 MergeHolds($dbh,$to_biblio, $from_biblio);
2021 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2023 =cut
2025 sub MergeHolds {
2026 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2027 my $sth = $dbh->prepare(
2028 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2030 $sth->execute($from_biblio);
2031 if ( my $data = $sth->fetchrow_hashref() ) {
2033 # holds exist on old record, if not we don't need to do anything
2034 $sth = $dbh->prepare(
2035 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2036 $sth->execute( $to_biblio, $from_biblio );
2038 # Reorder by date
2039 # don't reorder those already waiting
2041 $sth = $dbh->prepare(
2042 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2044 my $upd_sth = $dbh->prepare(
2045 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2046 AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2048 $sth->execute( $to_biblio, 'W', 'T' );
2049 my $priority = 1;
2050 while ( my $reserve = $sth->fetchrow_hashref() ) {
2051 $upd_sth->execute(
2052 $priority, $to_biblio,
2053 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2054 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2056 $priority++;
2061 =head2 RevertWaitingStatus
2063 $success = RevertWaitingStatus({ itemnumber => $itemnumber });
2065 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2067 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2068 item level hold, even if it was only a bibliolevel hold to
2069 begin with. This is because we can no longer know if a hold
2070 was item-level or bib-level after a hold has been set to
2071 waiting status.
2073 =cut
2075 sub RevertWaitingStatus {
2076 my ( $params ) = @_;
2077 my $itemnumber = $params->{'itemnumber'};
2079 return unless ( $itemnumber );
2081 my $dbh = C4::Context->dbh;
2083 ## Get the waiting reserve we want to revert
2084 my $query = "
2085 SELECT * FROM reserves
2086 WHERE itemnumber = ?
2087 AND found IS NOT NULL
2089 my $sth = $dbh->prepare( $query );
2090 $sth->execute( $itemnumber );
2091 my $reserve = $sth->fetchrow_hashref();
2093 ## Increment the priority of all other non-waiting
2094 ## reserves for this bib record
2095 $query = "
2096 UPDATE reserves
2098 priority = priority + 1
2099 WHERE
2100 biblionumber = ?
2102 priority > 0
2104 $sth = $dbh->prepare( $query );
2105 $sth->execute( $reserve->{'biblionumber'} );
2107 ## Fix up the currently waiting reserve
2108 $query = "
2109 UPDATE reserves
2111 priority = 1,
2112 found = NULL,
2113 waitingdate = NULL
2114 WHERE
2115 reserve_id = ?
2117 $sth = $dbh->prepare( $query );
2118 return $sth->execute( $reserve->{'reserve_id'} );
2121 =head2 GetReserveId
2123 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2125 Returnes the first reserve id that matches the given criteria
2127 =cut
2129 sub GetReserveId {
2130 my ( $params ) = @_;
2132 return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2134 my $dbh = C4::Context->dbh();
2136 my $sql = "SELECT reserve_id FROM reserves WHERE ";
2138 my @params;
2139 my @limits;
2140 foreach my $key ( keys %$params ) {
2141 if ( defined( $params->{$key} ) ) {
2142 push( @limits, "$key = ?" );
2143 push( @params, $params->{$key} );
2147 $sql .= join( " AND ", @limits );
2149 my $sth = $dbh->prepare( $sql );
2150 $sth->execute( @params );
2151 my $row = $sth->fetchrow_hashref();
2153 return $row->{'reserve_id'};
2156 =head2 ReserveSlip
2158 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2160 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2162 =cut
2164 sub ReserveSlip {
2165 my ($branch, $borrowernumber, $biblionumber) = @_;
2167 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2169 my $reserve = GetReserveInfo($borrowernumber,$biblionumber )
2170 or return;
2172 return C4::Letters::GetPreparedLetter (
2173 module => 'circulation',
2174 letter_code => 'RESERVESLIP',
2175 branchcode => $branch,
2176 tables => {
2177 'reserves' => $reserve,
2178 'branches' => $reserve->{branchcode},
2179 'borrowers' => $reserve->{borrowernumber},
2180 'biblio' => $reserve->{biblionumber},
2181 'items' => $reserve->{itemnumber},
2186 =head1 AUTHOR
2188 Koha Development Team <http://koha-community.org/>
2190 =cut