Bug 11614: Untranslatable label_element_title in label management
[koha.git] / C4 / Reserves.pm
bloba98eed4704d3cda0fc73b52640cfd832b399fd34
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 &GetReservesControlBranch
137 @EXPORT_OK = qw( MergeHolds );
140 =head2 AddReserve
142 AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
144 =cut
146 sub AddReserve {
147 my (
148 $branch, $borrowernumber, $biblionumber,
149 $constraint, $bibitems, $priority, $resdate, $expdate, $notes,
150 $title, $checkitem, $found
151 ) = @_;
152 my $fee =
153 GetReserveFee($borrowernumber, $biblionumber, $constraint,
154 $bibitems );
155 my $dbh = C4::Context->dbh;
156 my $const = lc substr( $constraint, 0, 1 );
157 $resdate = format_date_in_iso( $resdate ) if ( $resdate );
158 $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
159 if ($expdate) {
160 $expdate = format_date_in_iso( $expdate );
161 } else {
162 undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
164 if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
165 # Make room in reserves for this before those of a later reserve date
166 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
168 my $waitingdate;
170 # If the reserv had the waiting status, we had the value of the resdate
171 if ( $found eq 'W' ) {
172 $waitingdate = $resdate;
175 #eval {
176 # updates take place here
177 if ( $fee > 0 ) {
178 my $nextacctno = &getnextacctno( $borrowernumber );
179 my $query = qq/
180 INSERT INTO accountlines
181 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
182 VALUES
183 (?,?,now(),?,?,'Res',?)
185 my $usth = $dbh->prepare($query);
186 $usth->execute( $borrowernumber, $nextacctno, $fee,
187 "Reserve Charge - $title", $fee );
190 #if ($const eq 'a'){
191 my $query = qq/
192 INSERT INTO reserves
193 (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
194 priority,reservenotes,itemnumber,found,waitingdate,expirationdate)
195 VALUES
196 (?,?,?,?,?,
197 ?,?,?,?,?,?)
199 my $sth = $dbh->prepare($query);
200 $sth->execute(
201 $borrowernumber, $biblionumber, $resdate, $branch,
202 $const, $priority, $notes, $checkitem,
203 $found, $waitingdate, $expdate
206 # Send e-mail to librarian if syspref is active
207 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
208 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
209 my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode});
210 if ( my $letter = C4::Letters::GetPreparedLetter (
211 module => 'reserves',
212 letter_code => 'HOLDPLACED',
213 branchcode => $branch,
214 tables => {
215 'branches' => $branch_details,
216 'borrowers' => $borrower,
217 'biblio' => $biblionumber,
218 'items' => $checkitem,
220 ) ) {
222 my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
224 C4::Letters::EnqueueLetter(
225 { letter => $letter,
226 borrowernumber => $borrowernumber,
227 message_transport_type => 'email',
228 from_address => $admin_email_address,
229 to_address => $admin_email_address,
236 ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value?
237 $query = qq/
238 INSERT INTO reserveconstraints
239 (borrowernumber,biblionumber,reservedate,biblioitemnumber)
240 VALUES
241 (?,?,?,?)
243 $sth = $dbh->prepare($query); # keep prepare outside the loop!
244 foreach (@$bibitems) {
245 $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
248 return; # FIXME: why not have a useful return value?
251 =head2 GetReserve
253 $res = GetReserve( $reserve_id );
255 Return the current reserve.
257 =cut
259 sub GetReserve {
260 my ($reserve_id) = @_;
262 my $dbh = C4::Context->dbh;
263 my $query = "SELECT * FROM reserves WHERE reserve_id = ?";
264 my $sth = $dbh->prepare( $query );
265 $sth->execute( $reserve_id );
266 return $sth->fetchrow_hashref();
269 =head2 GetReservesFromBiblionumber
271 my $reserves = GetReservesFromBiblionumber({
272 biblionumber => $biblionumber,
273 [ itemnumber => $itemnumber, ]
274 [ all_dates => 1|0 ]
277 This function gets the list of reservations for one C<$biblionumber>,
278 returning an arrayref pointing to the reserves for C<$biblionumber>.
280 By default, only reserves whose start date falls before the current
281 time are returned. To return all reserves, including future ones,
282 the C<all_dates> parameter can be included and set to a true value.
284 If the C<itemnumber> parameter is supplied, reserves must be targeted
285 to that item or not targeted to any item at all; otherwise, they
286 are excluded from the list.
288 =cut
290 sub GetReservesFromBiblionumber {
291 my ( $params ) = @_;
292 my $biblionumber = $params->{biblionumber} or return [];
293 my $itemnumber = $params->{itemnumber};
294 my $all_dates = $params->{all_dates} // 0;
295 my $dbh = C4::Context->dbh;
297 # Find the desired items in the reserves
298 my @params;
299 my $query = "
300 SELECT reserve_id,
301 branchcode,
302 timestamp AS rtimestamp,
303 priority,
304 biblionumber,
305 borrowernumber,
306 reservedate,
307 constrainttype,
308 found,
309 itemnumber,
310 reservenotes,
311 expirationdate,
312 lowestPriority,
313 suspend,
314 suspend_until
315 FROM reserves
316 WHERE biblionumber = ? ";
317 push( @params, $biblionumber );
318 unless ( $all_dates ) {
319 $query .= " AND reservedate <= CAST(NOW() AS DATE) ";
321 if ( $itemnumber ) {
322 $query .= " AND ( itemnumber IS NULL OR itemnumber = ? )";
323 push( @params, $itemnumber );
325 $query .= "ORDER BY priority";
326 my $sth = $dbh->prepare($query);
327 $sth->execute( @params );
328 my @results;
329 my $i = 0;
330 while ( my $data = $sth->fetchrow_hashref ) {
332 # FIXME - What is this doing? How do constraints work?
333 if ($data->{constrainttype} eq 'o') {
334 $query = '
335 SELECT biblioitemnumber
336 FROM reserveconstraints
337 WHERE biblionumber = ?
338 AND borrowernumber = ?
339 AND reservedate = ?
341 my $csth = $dbh->prepare($query);
342 $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
343 my @bibitemno;
344 while ( my $bibitemnos = $csth->fetchrow_array ) {
345 push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref
347 my $count = scalar @bibitemno;
349 # if we have two or more different specific itemtypes
350 # reserved by same person on same day
351 my $bdata;
352 if ( $count > 1 ) {
353 $bdata = GetBiblioItemData( $bibitemno[$i] ); # FIXME: This doesn't make sense.
354 $i++; # $i can increase each pass, but the next @bibitemno might be smaller?
356 else {
357 # Look up the book we just found.
358 $bdata = GetBiblioItemData( $bibitemno[0] );
360 # Add the results of this latest search to the current
361 # results.
362 # FIXME - An 'each' would probably be more efficient.
363 foreach my $key ( keys %$bdata ) {
364 $data->{$key} = $bdata->{$key};
367 push @results, $data;
369 return \@results;
372 =head2 GetReservesFromItemnumber
374 ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $waitingdate ) = GetReservesFromItemnumber($itemnumber);
376 Get the first reserve for a specific item number (based on priority). Returns the abovementioned values for that reserve.
378 The routine does not look at future reserves (read: item level holds), but DOES include future waits (a confirmed future hold).
380 =cut
382 sub GetReservesFromItemnumber {
383 my ( $itemnumber ) = @_;
384 my $dbh = C4::Context->dbh;
385 my $query = "
386 SELECT reservedate,borrowernumber,branchcode,reserve_id,waitingdate
387 FROM reserves
388 WHERE itemnumber=? AND ( reservedate <= CAST(now() AS date) OR
389 waitingdate IS NOT NULL )
390 ORDER BY priority
392 my $sth_res = $dbh->prepare($query);
393 $sth_res->execute($itemnumber);
394 my ( $reservedate, $borrowernumber,$branchcode, $reserve_id, $wait ) = $sth_res->fetchrow_array;
395 return ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $wait );
398 =head2 GetReservesFromBorrowernumber
400 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
402 TODO :: Descritpion
404 =cut
406 sub GetReservesFromBorrowernumber {
407 my ( $borrowernumber, $status ) = @_;
408 my $dbh = C4::Context->dbh;
409 my $sth;
410 if ($status) {
411 $sth = $dbh->prepare("
412 SELECT *
413 FROM reserves
414 WHERE borrowernumber=?
415 AND found =?
416 ORDER BY reservedate
418 $sth->execute($borrowernumber,$status);
419 } else {
420 $sth = $dbh->prepare("
421 SELECT *
422 FROM reserves
423 WHERE borrowernumber=?
424 ORDER BY reservedate
426 $sth->execute($borrowernumber);
428 my $data = $sth->fetchall_arrayref({});
429 return @$data;
431 #-------------------------------------------------------------------------------------
432 =head2 CanBookBeReserved
434 $error = &CanBookBeReserved($borrowernumber, $biblionumber)
436 =cut
438 sub CanBookBeReserved{
439 my ($borrowernumber, $biblionumber) = @_;
441 my $items = GetItemnumbersForBiblio($biblionumber);
442 #get items linked via host records
443 my @hostitems = get_hostitemnumbers_of($biblionumber);
444 if (@hostitems){
445 push (@$items,@hostitems);
448 foreach my $item (@$items){
449 return 1 if CanItemBeReserved($borrowernumber, $item);
451 return 0;
454 =head2 CanItemBeReserved
456 $error = &CanItemBeReserved($borrowernumber, $itemnumber)
458 This function return 1 if an item can be issued by this borrower.
460 =cut
462 sub CanItemBeReserved{
463 my ($borrowernumber, $itemnumber) = @_;
465 my $dbh = C4::Context->dbh;
466 my $ruleitemtype; # itemtype of the matching issuing rule
467 my $allowedreserves = 0;
469 # we retrieve borrowers and items informations #
470 # item->{itype} will come for biblioitems if necessery
471 my $item = GetItem($itemnumber);
473 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
474 return 0 if ( $item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems') );
476 my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
478 my $controlbranch = C4::Context->preference('ReservesControlBranch');
479 my $itemtypefield = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype";
481 # we retrieve user rights on this itemtype and branchcode
482 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
483 FROM issuingrules
484 WHERE (categorycode in (?,'*') )
485 AND (itemtype IN (?,'*'))
486 AND (branchcode IN (?,'*'))
487 ORDER BY
488 categorycode DESC,
489 itemtype DESC,
490 branchcode DESC;"
493 my $querycount ="SELECT
494 count(*) as count
495 FROM reserves
496 LEFT JOIN items USING (itemnumber)
497 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
498 LEFT JOIN borrowers USING (borrowernumber)
499 WHERE borrowernumber = ?
503 my $branchcode = "";
504 my $branchfield = "reserves.branchcode";
506 if( $controlbranch eq "ItemHomeLibrary" ){
507 $branchfield = "items.homebranch";
508 $branchcode = $item->{homebranch};
509 }elsif( $controlbranch eq "PatronLibrary" ){
510 $branchfield = "borrowers.branchcode";
511 $branchcode = $borrower->{branchcode};
514 # we retrieve rights
515 $sth->execute($borrower->{'categorycode'}, $item->{'itype'}, $branchcode);
516 if(my $rights = $sth->fetchrow_hashref()){
517 $ruleitemtype = $rights->{itemtype};
518 $allowedreserves = $rights->{reservesallowed};
519 }else{
520 $ruleitemtype = '*';
523 # we retrieve count
525 $querycount .= "AND $branchfield = ?";
527 $querycount .= " AND $itemtypefield = ?" if ($ruleitemtype ne "*");
528 my $sthcount = $dbh->prepare($querycount);
530 if($ruleitemtype eq "*"){
531 $sthcount->execute($borrowernumber, $branchcode);
532 }else{
533 $sthcount->execute($borrowernumber, $branchcode, $ruleitemtype);
536 my $reservecount = "0";
537 if(my $rowcount = $sthcount->fetchrow_hashref()){
538 $reservecount = $rowcount->{count};
541 # we check if it's ok or not
542 if( $reservecount >= $allowedreserves ){
543 return 0;
546 # If reservecount is ok, we check item branch if IndependentBranches is ON
547 # and canreservefromotherbranches is OFF
548 if ( C4::Context->preference('IndependentBranches')
549 and !C4::Context->preference('canreservefromotherbranches') )
551 my $itembranch = $item->{homebranch};
552 if ($itembranch ne $borrower->{branchcode}) {
553 return 0;
557 return 1;
559 #--------------------------------------------------------------------------------
560 =head2 GetReserveCount
562 $number = &GetReserveCount($borrowernumber);
564 this function returns the number of reservation for a borrower given on input arg.
566 =cut
568 sub GetReserveCount {
569 my ($borrowernumber) = @_;
571 my $dbh = C4::Context->dbh;
573 my $query = "
574 SELECT COUNT(*) AS counter
575 FROM reserves
576 WHERE borrowernumber = ?
578 my $sth = $dbh->prepare($query);
579 $sth->execute($borrowernumber);
580 my $row = $sth->fetchrow_hashref;
581 return $row->{counter};
584 =head2 GetOtherReserves
586 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
588 Check queued list of this document and check if this document must be transfered
590 =cut
592 sub GetOtherReserves {
593 my ($itemnumber) = @_;
594 my $messages;
595 my $nextreservinfo;
596 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
597 if ($checkreserves) {
598 my $iteminfo = GetItem($itemnumber);
599 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
600 $messages->{'transfert'} = $checkreserves->{'branchcode'};
601 #minus priorities of others reservs
602 ModReserveMinusPriority(
603 $itemnumber,
604 $checkreserves->{'reserve_id'},
607 #launch the subroutine dotransfer
608 C4::Items::ModItemTransfer(
609 $itemnumber,
610 $iteminfo->{'holdingbranch'},
611 $checkreserves->{'branchcode'}
616 #step 2b : case of a reservation on the same branch, set the waiting status
617 else {
618 $messages->{'waiting'} = 1;
619 ModReserveMinusPriority(
620 $itemnumber,
621 $checkreserves->{'reserve_id'},
623 ModReserveStatus($itemnumber,'W');
626 $nextreservinfo = $checkreserves->{'borrowernumber'};
629 return ( $messages, $nextreservinfo );
632 =head2 GetReserveFee
634 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
636 Calculate the fee for a reserve
638 =cut
640 sub GetReserveFee {
641 my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
643 #check for issues;
644 my $dbh = C4::Context->dbh;
645 my $const = lc substr( $constraint, 0, 1 );
646 my $query = qq/
647 SELECT * FROM borrowers
648 LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
649 WHERE borrowernumber = ?
651 my $sth = $dbh->prepare($query);
652 $sth->execute($borrowernumber);
653 my $data = $sth->fetchrow_hashref;
654 my $fee = $data->{'reservefee'};
655 my $cntitems = @- > $bibitems;
657 if ( $fee > 0 ) {
659 # check for items on issue
660 # first find biblioitem records
661 my @biblioitems;
662 my $sth1 = $dbh->prepare(
663 "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
664 WHERE (biblio.biblionumber = ?)"
666 $sth1->execute($biblionumber);
667 while ( my $data1 = $sth1->fetchrow_hashref ) {
668 if ( $const eq "a" ) {
669 push @biblioitems, $data1;
671 else {
672 my $found = 0;
673 my $x = 0;
674 while ( $x < $cntitems ) {
675 if ( @$bibitems->{'biblioitemnumber'} ==
676 $data->{'biblioitemnumber'} )
678 $found = 1;
680 $x++;
682 if ( $const eq 'o' ) {
683 if ( $found == 1 ) {
684 push @biblioitems, $data1;
687 else {
688 if ( $found == 0 ) {
689 push @biblioitems, $data1;
694 my $cntitemsfound = @biblioitems;
695 my $issues = 0;
696 my $x = 0;
697 my $allissued = 1;
698 while ( $x < $cntitemsfound ) {
699 my $bitdata = $biblioitems[$x];
700 my $sth2 = $dbh->prepare(
701 "SELECT * FROM items
702 WHERE biblioitemnumber = ?"
704 $sth2->execute( $bitdata->{'biblioitemnumber'} );
705 while ( my $itdata = $sth2->fetchrow_hashref ) {
706 my $sth3 = $dbh->prepare(
707 "SELECT * FROM issues
708 WHERE itemnumber = ?"
710 $sth3->execute( $itdata->{'itemnumber'} );
711 if ( my $isdata = $sth3->fetchrow_hashref ) {
713 else {
714 $allissued = 0;
717 $x++;
719 if ( $allissued == 0 ) {
720 my $rsth =
721 $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
722 $rsth->execute($biblionumber);
723 if ( my $rdata = $rsth->fetchrow_hashref ) {
725 else {
726 $fee = 0;
730 return $fee;
733 =head2 GetReservesToBranch
735 @transreserv = GetReservesToBranch( $frombranch );
737 Get reserve list for a given branch
739 =cut
741 sub GetReservesToBranch {
742 my ( $frombranch ) = @_;
743 my $dbh = C4::Context->dbh;
744 my $sth = $dbh->prepare(
745 "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
746 FROM reserves
747 WHERE priority='0'
748 AND branchcode=?"
750 $sth->execute( $frombranch );
751 my @transreserv;
752 my $i = 0;
753 while ( my $data = $sth->fetchrow_hashref ) {
754 $transreserv[$i] = $data;
755 $i++;
757 return (@transreserv);
760 =head2 GetReservesForBranch
762 @transreserv = GetReservesForBranch($frombranch);
764 =cut
766 sub GetReservesForBranch {
767 my ($frombranch) = @_;
768 my $dbh = C4::Context->dbh;
770 my $query = "
771 SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
772 FROM reserves
773 WHERE priority='0'
774 AND found='W'
776 $query .= " AND branchcode=? " if ( $frombranch );
777 $query .= "ORDER BY waitingdate" ;
779 my $sth = $dbh->prepare($query);
780 if ($frombranch){
781 $sth->execute($frombranch);
782 } else {
783 $sth->execute();
786 my @transreserv;
787 my $i = 0;
788 while ( my $data = $sth->fetchrow_hashref ) {
789 $transreserv[$i] = $data;
790 $i++;
792 return (@transreserv);
795 =head2 GetReserveStatus
797 $reservestatus = GetReserveStatus($itemnumber, $biblionumber);
799 Take an itemnumber or a biblionumber and return the status of the reserve places on it.
800 If several reserves exist, the reserve with the lower priority is given.
802 =cut
804 ## FIXME: I don't think this does what it thinks it does.
805 ## It only ever checks the first reserve result, even though
806 ## multiple reserves for that bib can have the itemnumber set
807 ## the sub is only used once in the codebase.
808 sub GetReserveStatus {
809 my ($itemnumber, $biblionumber) = @_;
811 my $dbh = C4::Context->dbh;
813 my ($sth, $found, $priority);
814 if ( $itemnumber ) {
815 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
816 $sth->execute($itemnumber);
817 ($found, $priority) = $sth->fetchrow_array;
820 if ( $biblionumber and not defined $found and not defined $priority ) {
821 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE biblionumber = ? order by priority LIMIT 1");
822 $sth->execute($biblionumber);
823 ($found, $priority) = $sth->fetchrow_array;
826 if(defined $found) {
827 return 'Waiting' if $found eq 'W' and $priority == 0;
828 return 'Finished' if $found eq 'F';
831 return 'Reserved' if $priority > 0;
833 return ''; # empty string here will remove need for checking undef, or less log lines
836 =head2 CheckReserves
838 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
839 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
840 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
842 Find a book in the reserves.
844 C<$itemnumber> is the book's item number.
845 C<$lookahead> is the number of days to look in advance for future reserves.
847 As I understand it, C<&CheckReserves> looks for the given item in the
848 reserves. If it is found, that's a match, and C<$status> is set to
849 C<Waiting>.
851 Otherwise, it finds the most important item in the reserves with the
852 same biblio number as this book (I'm not clear on this) and returns it
853 with C<$status> set to C<Reserved>.
855 C<&CheckReserves> returns a two-element list:
857 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
859 C<$reserve> is the reserve item that matched. It is a
860 reference-to-hash whose keys are mostly the fields of the reserves
861 table in the Koha database.
863 =cut
865 sub CheckReserves {
866 my ( $item, $barcode, $lookahead_days) = @_;
867 my $dbh = C4::Context->dbh;
868 my $sth;
869 my $select;
870 if (C4::Context->preference('item-level_itypes')){
871 $select = "
872 SELECT items.biblionumber,
873 items.biblioitemnumber,
874 itemtypes.notforloan,
875 items.notforloan AS itemnotforloan,
876 items.itemnumber,
877 items.damaged
878 FROM items
879 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
880 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
883 else {
884 $select = "
885 SELECT items.biblionumber,
886 items.biblioitemnumber,
887 itemtypes.notforloan,
888 items.notforloan AS itemnotforloan,
889 items.itemnumber,
890 items.damaged
891 FROM items
892 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
893 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
897 if ($item) {
898 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
899 $sth->execute($item);
901 else {
902 $sth = $dbh->prepare("$select WHERE barcode = ?");
903 $sth->execute($barcode);
905 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
906 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged ) = $sth->fetchrow_array;
908 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
910 return unless $itemnumber; # bail if we got nothing.
912 # if item is not for loan it cannot be reserved either.....
913 # execpt where items.notforloan < 0 : This indicates the item is holdable.
914 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
916 # Find this item in the reserves
917 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days);
919 # $priority and $highest are used to find the most important item
920 # in the list returned by &_Findgroupreserve. (The lower $priority,
921 # the more important the item.)
922 # $highest is the most important item we've seen so far.
923 my $highest;
924 if (scalar @reserves) {
925 my $priority = 10000000;
926 foreach my $res (@reserves) {
927 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
928 return ( "Waiting", $res, \@reserves ); # Found it
929 } else {
930 # See if this item is more important than what we've got so far
931 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
932 my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'});
933 my $iteminfo=C4::Items::GetItem($itemnumber);
934 my $branch = GetReservesControlBranch( $iteminfo, $borrowerinfo );
935 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
936 next if ($branchitemrule->{'holdallowed'} == 0);
937 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
938 $priority = $res->{'priority'};
939 $highest = $res;
945 # If we get this far, then no exact match was found.
946 # We return the most important (i.e. next) reservation.
947 if ($highest) {
948 $highest->{'itemnumber'} = $item;
949 return ( "Reserved", $highest, \@reserves );
952 return ( '' );
955 =head2 CancelExpiredReserves
957 CancelExpiredReserves();
959 Cancels all reserves with an expiration date from before today.
961 =cut
963 sub CancelExpiredReserves {
965 # Cancel reserves that have passed their expiration date.
966 my $dbh = C4::Context->dbh;
967 my $sth = $dbh->prepare( "
968 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
969 AND expirationdate IS NOT NULL
970 AND found IS NULL
971 " );
972 $sth->execute();
974 while ( my $res = $sth->fetchrow_hashref() ) {
975 CancelReserve({ reserve_id => $res->{'reserve_id'} });
978 # Cancel reserves that have been waiting too long
979 if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
980 my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
981 my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
983 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
984 $sth = $dbh->prepare( $query );
985 $sth->execute( $max_pickup_delay );
987 while (my $res = $sth->fetchrow_hashref ) {
988 if ( $charge ) {
989 manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
992 CancelReserve({ reserve_id => $res->{'reserve_id'} });
998 =head2 AutoUnsuspendReserves
1000 AutoUnsuspendReserves();
1002 Unsuspends all suspended reserves with a suspend_until date from before today.
1004 =cut
1006 sub AutoUnsuspendReserves {
1008 my $dbh = C4::Context->dbh;
1010 my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
1011 my $sth = $dbh->prepare( $query );
1012 $sth->execute();
1016 =head2 CancelReserve
1018 CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber ] });
1020 Cancels a reserve.
1022 =cut
1024 sub CancelReserve {
1025 my ( $params ) = @_;
1027 my $reserve_id = $params->{'reserve_id'};
1028 $reserve_id = GetReserveId( $params ) unless ( $reserve_id );
1030 return unless ( $reserve_id );
1032 my $dbh = C4::Context->dbh;
1034 my $reserve = GetReserve( $reserve_id );
1036 my $query = "
1037 UPDATE reserves
1038 SET cancellationdate = now(),
1039 found = Null,
1040 priority = 0
1041 WHERE reserve_id = ?
1043 my $sth = $dbh->prepare($query);
1044 $sth->execute( $reserve_id );
1046 $query = "
1047 INSERT INTO old_reserves
1048 SELECT * FROM reserves
1049 WHERE reserve_id = ?
1051 $sth = $dbh->prepare($query);
1052 $sth->execute( $reserve_id );
1054 $query = "
1055 DELETE FROM reserves
1056 WHERE reserve_id = ?
1058 $sth = $dbh->prepare($query);
1059 $sth->execute( $reserve_id );
1061 # now fix the priority on the others....
1062 _FixPriority({ biblionumber => $reserve->{biblionumber} });
1065 =head2 ModReserve
1067 ModReserve({ rank => $rank,
1068 reserve_id => $reserve_id,
1069 branchcode => $branchcode
1070 [, itemnumber => $itemnumber ]
1071 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1074 Change a hold request's priority or cancel it.
1076 C<$rank> specifies the effect of the change. If C<$rank>
1077 is 'W' or 'n', nothing happens. This corresponds to leaving a
1078 request alone when changing its priority in the holds queue
1079 for a bib.
1081 If C<$rank> is 'del', the hold request is cancelled.
1083 If C<$rank> is an integer greater than zero, the priority of
1084 the request is set to that value. Since priority != 0 means
1085 that the item is not waiting on the hold shelf, setting the
1086 priority to a non-zero value also sets the request's found
1087 status and waiting date to NULL.
1089 The optional C<$itemnumber> parameter is used only when
1090 C<$rank> is a non-zero integer; if supplied, the itemnumber
1091 of the hold request is set accordingly; if omitted, the itemnumber
1092 is cleared.
1094 B<FIXME:> Note that the forgoing can have the effect of causing
1095 item-level hold requests to turn into title-level requests. This
1096 will be fixed once reserves has separate columns for requested
1097 itemnumber and supplying itemnumber.
1099 =cut
1101 sub ModReserve {
1102 my ( $params ) = @_;
1104 my $rank = $params->{'rank'};
1105 my $reserve_id = $params->{'reserve_id'};
1106 my $branchcode = $params->{'branchcode'};
1107 my $itemnumber = $params->{'itemnumber'};
1108 my $suspend_until = $params->{'suspend_until'};
1109 my $borrowernumber = $params->{'borrowernumber'};
1110 my $biblionumber = $params->{'biblionumber'};
1112 return if $rank eq "W";
1113 return if $rank eq "n";
1115 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1116 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1118 my $dbh = C4::Context->dbh;
1119 if ( $rank eq "del" ) {
1120 CancelReserve({ reserve_id => $reserve_id });
1122 elsif ($rank =~ /^\d+/ and $rank > 0) {
1123 my $query = "
1124 UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1125 WHERE reserve_id = ?
1127 my $sth = $dbh->prepare($query);
1128 $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
1130 if ( defined( $suspend_until ) ) {
1131 if ( $suspend_until ) {
1132 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1133 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
1134 } else {
1135 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
1139 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1143 =head2 ModReserveFill
1145 &ModReserveFill($reserve);
1147 Fill a reserve. If I understand this correctly, this means that the
1148 reserved book has been found and given to the patron who reserved it.
1150 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1151 whose keys are fields from the reserves table in the Koha database.
1153 =cut
1155 sub ModReserveFill {
1156 my ($res) = @_;
1157 my $dbh = C4::Context->dbh;
1158 # fill in a reserve record....
1159 my $reserve_id = $res->{'reserve_id'};
1160 my $biblionumber = $res->{'biblionumber'};
1161 my $borrowernumber = $res->{'borrowernumber'};
1162 my $resdate = $res->{'reservedate'};
1164 # get the priority on this record....
1165 my $priority;
1166 my $query = "SELECT priority
1167 FROM reserves
1168 WHERE biblionumber = ?
1169 AND borrowernumber = ?
1170 AND reservedate = ?";
1171 my $sth = $dbh->prepare($query);
1172 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1173 ($priority) = $sth->fetchrow_array;
1175 # update the database...
1176 $query = "UPDATE reserves
1177 SET found = 'F',
1178 priority = 0
1179 WHERE biblionumber = ?
1180 AND reservedate = ?
1181 AND borrowernumber = ?
1183 $sth = $dbh->prepare($query);
1184 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1186 # move to old_reserves
1187 $query = "INSERT INTO old_reserves
1188 SELECT * FROM reserves
1189 WHERE biblionumber = ?
1190 AND reservedate = ?
1191 AND borrowernumber = ?
1193 $sth = $dbh->prepare($query);
1194 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1195 $query = "DELETE FROM reserves
1196 WHERE biblionumber = ?
1197 AND reservedate = ?
1198 AND borrowernumber = ?
1200 $sth = $dbh->prepare($query);
1201 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1203 # now fix the priority on the others (if the priority wasn't
1204 # already sorted!)....
1205 unless ( $priority == 0 ) {
1206 _FixPriority({ reserve_id => $reserve_id });
1210 =head2 ModReserveStatus
1212 &ModReserveStatus($itemnumber, $newstatus);
1214 Update the reserve status for the active (priority=0) reserve.
1216 $itemnumber is the itemnumber the reserve is on
1218 $newstatus is the new status.
1220 =cut
1222 sub ModReserveStatus {
1224 #first : check if we have a reservation for this item .
1225 my ($itemnumber, $newstatus) = @_;
1226 my $dbh = C4::Context->dbh;
1228 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1229 my $sth_set = $dbh->prepare($query);
1230 $sth_set->execute( $newstatus, $itemnumber );
1232 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1233 CartToShelf( $itemnumber );
1237 =head2 ModReserveAffect
1239 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1241 This function affect an item and a status for a given reserve
1242 The itemnumber parameter is used to find the biblionumber.
1243 with the biblionumber & the borrowernumber, we can affect the itemnumber
1244 to the correct reserve.
1246 if $transferToDo is not set, then the status is set to "Waiting" as well.
1247 otherwise, a transfer is on the way, and the end of the transfer will
1248 take care of the waiting status
1250 =cut
1252 sub ModReserveAffect {
1253 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1254 my $dbh = C4::Context->dbh;
1256 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1257 # attached to $itemnumber
1258 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1259 $sth->execute($itemnumber);
1260 my ($biblionumber) = $sth->fetchrow;
1262 # get request - need to find out if item is already
1263 # waiting in order to not send duplicate hold filled notifications
1264 my $reserve_id = GetReserveId({
1265 borrowernumber => $borrowernumber,
1266 biblionumber => $biblionumber,
1268 return unless defined $reserve_id;
1269 my $request = GetReserveInfo($reserve_id);
1270 my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1272 # If we affect a reserve that has to be transfered, don't set to Waiting
1273 my $query;
1274 if ($transferToDo) {
1275 $query = "
1276 UPDATE reserves
1277 SET priority = 0,
1278 itemnumber = ?,
1279 found = 'T'
1280 WHERE borrowernumber = ?
1281 AND biblionumber = ?
1284 else {
1285 # affect the reserve to Waiting as well.
1286 $query = "
1287 UPDATE reserves
1288 SET priority = 0,
1289 found = 'W',
1290 waitingdate = NOW(),
1291 itemnumber = ?
1292 WHERE borrowernumber = ?
1293 AND biblionumber = ?
1296 $sth = $dbh->prepare($query);
1297 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1298 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1299 _FixPriority( { biblionumber => $biblionumber } );
1300 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1301 CartToShelf( $itemnumber );
1304 return;
1307 =head2 ModReserveCancelAll
1309 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1311 function to cancel reserv,check other reserves, and transfer document if it's necessary
1313 =cut
1315 sub ModReserveCancelAll {
1316 my $messages;
1317 my $nextreservinfo;
1318 my ( $itemnumber, $borrowernumber ) = @_;
1320 #step 1 : cancel the reservation
1321 my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1323 #step 2 launch the subroutine of the others reserves
1324 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1326 return ( $messages, $nextreservinfo );
1329 =head2 ModReserveMinusPriority
1331 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1333 Reduce the values of queued list
1335 =cut
1337 sub ModReserveMinusPriority {
1338 my ( $itemnumber, $reserve_id ) = @_;
1340 #first step update the value of the first person on reserv
1341 my $dbh = C4::Context->dbh;
1342 my $query = "
1343 UPDATE reserves
1344 SET priority = 0 , itemnumber = ?
1345 WHERE reserve_id = ?
1347 my $sth_upd = $dbh->prepare($query);
1348 $sth_upd->execute( $itemnumber, $reserve_id );
1349 # second step update all others reserves
1350 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1353 =head2 GetReserveInfo
1355 &GetReserveInfo($reserve_id);
1357 Get item and borrower details for a current hold.
1358 Current implementation this query should have a single result.
1360 =cut
1362 sub GetReserveInfo {
1363 my ( $reserve_id ) = @_;
1364 my $dbh = C4::Context->dbh;
1365 my $strsth="SELECT
1366 reserve_id,
1367 reservedate,
1368 reservenotes,
1369 reserves.borrowernumber,
1370 reserves.biblionumber,
1371 reserves.branchcode,
1372 reserves.waitingdate,
1373 notificationdate,
1374 reminderdate,
1375 priority,
1376 found,
1377 firstname,
1378 surname,
1379 phone,
1380 email,
1381 address,
1382 address2,
1383 cardnumber,
1384 city,
1385 zipcode,
1386 biblio.title,
1387 biblio.author,
1388 items.holdingbranch,
1389 items.itemcallnumber,
1390 items.itemnumber,
1391 items.location,
1392 barcode,
1393 notes
1394 FROM reserves
1395 LEFT JOIN items USING(itemnumber)
1396 LEFT JOIN borrowers USING(borrowernumber)
1397 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1398 WHERE reserves.reserve_id = ?";
1399 my $sth = $dbh->prepare($strsth);
1400 $sth->execute($reserve_id);
1402 my $data = $sth->fetchrow_hashref;
1403 return $data;
1406 =head2 IsAvailableForItemLevelRequest
1408 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1410 Checks whether a given item record is available for an
1411 item-level hold request. An item is available if
1413 * it is not lost AND
1414 * it is not damaged AND
1415 * it is not withdrawn AND
1416 * does not have a not for loan value > 0
1418 Whether or not the item is currently on loan is
1419 also checked - if the AllowOnShelfHolds system preference
1420 is ON, an item can be requested even if it is currently
1421 on loan to somebody else. If the system preference
1422 is OFF, an item that is currently checked out cannot
1423 be the target of an item-level hold request.
1425 Note that IsAvailableForItemLevelRequest() does not
1426 check if the staff operator is authorized to place
1427 a request on the item - in particular,
1428 this routine does not check IndependentBranches
1429 and canreservefromotherbranches.
1431 =cut
1433 sub IsAvailableForItemLevelRequest {
1434 my $itemnumber = shift;
1436 my $item = GetItem($itemnumber);
1438 # must check the notforloan setting of the itemtype
1439 # FIXME - a lot of places in the code do this
1440 # or something similar - need to be
1441 # consolidated
1442 my $dbh = C4::Context->dbh;
1443 my $notforloan_query;
1444 if (C4::Context->preference('item-level_itypes')) {
1445 $notforloan_query = "SELECT itemtypes.notforloan
1446 FROM items
1447 JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1448 WHERE itemnumber = ?";
1449 } else {
1450 $notforloan_query = "SELECT itemtypes.notforloan
1451 FROM items
1452 JOIN biblioitems USING (biblioitemnumber)
1453 JOIN itemtypes USING (itemtype)
1454 WHERE itemnumber = ?";
1456 my $sth = $dbh->prepare($notforloan_query);
1457 $sth->execute($itemnumber);
1458 my $notforloan_per_itemtype = 0;
1459 if (my ($notforloan) = $sth->fetchrow_array) {
1460 $notforloan_per_itemtype = 1 if $notforloan;
1463 my $available_per_item = 1;
1464 $available_per_item = 0 if $item->{itemlost} or
1465 ( $item->{notforloan} > 0 ) or
1466 ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1467 $item->{withdrawn} or
1468 $notforloan_per_itemtype;
1471 if (C4::Context->preference('AllowOnShelfHolds')) {
1472 return $available_per_item;
1473 } else {
1474 return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "Waiting"));
1478 =head2 AlterPriority
1480 AlterPriority( $where, $reserve_id );
1482 This function changes a reserve's priority up, down, to the top, or to the bottom.
1483 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1485 =cut
1487 sub AlterPriority {
1488 my ( $where, $reserve_id ) = @_;
1490 my $dbh = C4::Context->dbh;
1492 my $reserve = GetReserve( $reserve_id );
1494 if ( $reserve->{cancellationdate} ) {
1495 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1496 return;
1499 if ( $where eq 'up' || $where eq 'down' ) {
1501 my $priority = $reserve->{'priority'};
1502 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1503 _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1505 } elsif ( $where eq 'top' ) {
1507 _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1509 } elsif ( $where eq 'bottom' ) {
1511 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1516 =head2 ToggleLowestPriority
1518 ToggleLowestPriority( $borrowernumber, $biblionumber );
1520 This function sets the lowestPriority field to true if is false, and false if it is true.
1522 =cut
1524 sub ToggleLowestPriority {
1525 my ( $reserve_id ) = @_;
1527 my $dbh = C4::Context->dbh;
1529 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1530 $sth->execute( $reserve_id );
1532 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1535 =head2 ToggleSuspend
1537 ToggleSuspend( $reserve_id );
1539 This function sets the suspend field to true if is false, and false if it is true.
1540 If the reserve is currently suspended with a suspend_until date, that date will
1541 be cleared when it is unsuspended.
1543 =cut
1545 sub ToggleSuspend {
1546 my ( $reserve_id, $suspend_until ) = @_;
1548 $suspend_until = output_pref(
1550 dt => dt_from_string($suspend_until),
1551 dateformat => 'iso',
1552 dateonly => 1
1554 ) if ($suspend_until);
1556 my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1558 my $dbh = C4::Context->dbh;
1560 my $sth = $dbh->prepare(
1561 "UPDATE reserves SET suspend = NOT suspend,
1562 suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1563 WHERE reserve_id = ?
1566 my @params;
1567 push( @params, $suspend_until ) if ( $suspend_until );
1568 push( @params, $reserve_id );
1570 $sth->execute( @params );
1573 =head2 SuspendAll
1575 SuspendAll(
1576 borrowernumber => $borrowernumber,
1577 [ biblionumber => $biblionumber, ]
1578 [ suspend_until => $suspend_until, ]
1579 [ suspend => $suspend ]
1582 This function accepts a set of hash keys as its parameters.
1583 It requires either borrowernumber or biblionumber, or both.
1585 suspend_until is wholly optional.
1587 =cut
1589 sub SuspendAll {
1590 my %params = @_;
1592 my $borrowernumber = $params{'borrowernumber'} || undef;
1593 my $biblionumber = $params{'biblionumber'} || undef;
1594 my $suspend_until = $params{'suspend_until'} || undef;
1595 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1597 $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1599 return unless ( $borrowernumber || $biblionumber );
1601 my ( $query, $sth, $dbh, @query_params );
1603 $query = "UPDATE reserves SET suspend = ? ";
1604 push( @query_params, $suspend );
1605 if ( !$suspend ) {
1606 $query .= ", suspend_until = NULL ";
1607 } elsif ( $suspend_until ) {
1608 $query .= ", suspend_until = ? ";
1609 push( @query_params, $suspend_until );
1611 $query .= " WHERE ";
1612 if ( $borrowernumber ) {
1613 $query .= " borrowernumber = ? ";
1614 push( @query_params, $borrowernumber );
1616 $query .= " AND " if ( $borrowernumber && $biblionumber );
1617 if ( $biblionumber ) {
1618 $query .= " biblionumber = ? ";
1619 push( @query_params, $biblionumber );
1621 $query .= " AND found IS NULL ";
1623 $dbh = C4::Context->dbh;
1624 $sth = $dbh->prepare( $query );
1625 $sth->execute( @query_params );
1629 =head2 _FixPriority
1631 _FixPriority({
1632 reserve_id => $reserve_id,
1633 [rank => $rank,]
1634 [ignoreSetLowestRank => $ignoreSetLowestRank]
1639 _FixPriority({ biblionumber => $biblionumber});
1641 This routine adjusts the priority of a hold request and holds
1642 on the same bib.
1644 In the first form, where a reserve_id is passed, the priority of the
1645 hold is set to supplied rank, and other holds for that bib are adjusted
1646 accordingly. If the rank is "del", the hold is cancelled. If no rank
1647 is supplied, all of the holds on that bib have their priority adjusted
1648 as if the second form had been used.
1650 In the second form, where a biblionumber is passed, the holds on that
1651 bib (that are not captured) are sorted in order of increasing priority,
1652 then have reserves.priority set so that the first non-captured hold
1653 has its priority set to 1, the second non-captured hold has its priority
1654 set to 2, and so forth.
1656 In both cases, holds that have the lowestPriority flag on are have their
1657 priority adjusted to ensure that they remain at the end of the line.
1659 Note that the ignoreSetLowestRank parameter is meant to be used only
1660 when _FixPriority calls itself.
1662 =cut
1664 sub _FixPriority {
1665 my ( $params ) = @_;
1666 my $reserve_id = $params->{reserve_id};
1667 my $rank = $params->{rank} // '';
1668 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1669 my $biblionumber = $params->{biblionumber};
1671 my $dbh = C4::Context->dbh;
1673 unless ( $biblionumber ) {
1674 my $res = GetReserve( $reserve_id );
1675 $biblionumber = $res->{biblionumber};
1678 if ( $rank eq "del" ) {
1679 CancelReserve({ reserve_id => $reserve_id });
1681 elsif ( $rank eq "W" || $rank eq "0" ) {
1683 # make sure priority for waiting or in-transit items is 0
1684 my $query = "
1685 UPDATE reserves
1686 SET priority = 0
1687 WHERE reserve_id = ?
1688 AND found IN ('W', 'T')
1690 my $sth = $dbh->prepare($query);
1691 $sth->execute( $reserve_id );
1693 my @priority;
1695 # get whats left
1696 my $query = "
1697 SELECT reserve_id, borrowernumber, reservedate, constrainttype
1698 FROM reserves
1699 WHERE biblionumber = ?
1700 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1701 ORDER BY priority ASC
1703 my $sth = $dbh->prepare($query);
1704 $sth->execute( $biblionumber );
1705 while ( my $line = $sth->fetchrow_hashref ) {
1706 push( @priority, $line );
1709 # To find the matching index
1710 my $i;
1711 my $key = -1; # to allow for 0 to be a valid result
1712 for ( $i = 0 ; $i < @priority ; $i++ ) {
1713 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1714 $key = $i; # save the index
1715 last;
1719 # if index exists in array then move it to new position
1720 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1721 my $new_rank = $rank -
1722 1; # $new_rank is what you want the new index to be in the array
1723 my $moving_item = splice( @priority, $key, 1 );
1724 splice( @priority, $new_rank, 0, $moving_item );
1727 # now fix the priority on those that are left....
1728 $query = "
1729 UPDATE reserves
1730 SET priority = ?
1731 WHERE reserve_id = ?
1733 $sth = $dbh->prepare($query);
1734 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1735 $sth->execute(
1736 $j + 1,
1737 $priority[$j]->{'reserve_id'}
1741 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1742 $sth->execute();
1744 unless ( $ignoreSetLowestRank ) {
1745 while ( my $res = $sth->fetchrow_hashref() ) {
1746 _FixPriority({
1747 reserve_id => $res->{'reserve_id'},
1748 rank => '999999',
1749 ignoreSetLowestRank => 1
1755 =head2 _Findgroupreserve
1757 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead);
1759 Looks for an item-specific match first, then for a title-level match, returning the
1760 first match found. If neither, then we look for a 3rd kind of match based on
1761 reserve constraints.
1762 Lookahead is the number of days to look in advance.
1764 TODO: add more explanation about reserve constraints
1766 C<&_Findgroupreserve> returns :
1767 C<@results> is an array of references-to-hash whose keys are mostly
1768 fields from the reserves table of the Koha database, plus
1769 C<biblioitemnumber>.
1771 =cut
1773 sub _Findgroupreserve {
1774 my ( $bibitem, $biblio, $itemnumber, $lookahead) = @_;
1775 my $dbh = C4::Context->dbh;
1777 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1778 # check for exact targetted match
1779 my $item_level_target_query = qq/
1780 SELECT reserves.biblionumber AS biblionumber,
1781 reserves.borrowernumber AS borrowernumber,
1782 reserves.reservedate AS reservedate,
1783 reserves.branchcode AS branchcode,
1784 reserves.cancellationdate AS cancellationdate,
1785 reserves.found AS found,
1786 reserves.reservenotes AS reservenotes,
1787 reserves.priority AS priority,
1788 reserves.timestamp AS timestamp,
1789 biblioitems.biblioitemnumber AS biblioitemnumber,
1790 reserves.itemnumber AS itemnumber,
1791 reserves.reserve_id AS reserve_id
1792 FROM reserves
1793 JOIN biblioitems USING (biblionumber)
1794 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1795 WHERE found IS NULL
1796 AND priority > 0
1797 AND item_level_request = 1
1798 AND itemnumber = ?
1799 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1800 AND suspend = 0
1802 my $sth = $dbh->prepare($item_level_target_query);
1803 $sth->execute($itemnumber, $lookahead||0);
1804 my @results;
1805 if ( my $data = $sth->fetchrow_hashref ) {
1806 push( @results, $data );
1808 return @results if @results;
1810 # check for title-level targetted match
1811 my $title_level_target_query = qq/
1812 SELECT reserves.biblionumber AS biblionumber,
1813 reserves.borrowernumber AS borrowernumber,
1814 reserves.reservedate AS reservedate,
1815 reserves.branchcode AS branchcode,
1816 reserves.cancellationdate AS cancellationdate,
1817 reserves.found AS found,
1818 reserves.reservenotes AS reservenotes,
1819 reserves.priority AS priority,
1820 reserves.timestamp AS timestamp,
1821 biblioitems.biblioitemnumber AS biblioitemnumber,
1822 reserves.itemnumber AS itemnumber,
1823 reserves.reserve_id AS reserve_id
1824 FROM reserves
1825 JOIN biblioitems USING (biblionumber)
1826 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1827 WHERE found IS NULL
1828 AND priority > 0
1829 AND item_level_request = 0
1830 AND hold_fill_targets.itemnumber = ?
1831 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1832 AND suspend = 0
1834 $sth = $dbh->prepare($title_level_target_query);
1835 $sth->execute($itemnumber, $lookahead||0);
1836 @results = ();
1837 if ( my $data = $sth->fetchrow_hashref ) {
1838 push( @results, $data );
1840 return @results if @results;
1842 my $query = qq/
1843 SELECT reserves.biblionumber AS biblionumber,
1844 reserves.borrowernumber AS borrowernumber,
1845 reserves.reservedate AS reservedate,
1846 reserves.waitingdate AS waitingdate,
1847 reserves.branchcode AS branchcode,
1848 reserves.cancellationdate AS cancellationdate,
1849 reserves.found AS found,
1850 reserves.reservenotes AS reservenotes,
1851 reserves.priority AS priority,
1852 reserves.timestamp AS timestamp,
1853 reserveconstraints.biblioitemnumber AS biblioitemnumber,
1854 reserves.itemnumber AS itemnumber,
1855 reserves.reserve_id AS reserve_id
1856 FROM reserves
1857 LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1858 WHERE reserves.biblionumber = ?
1859 AND ( ( reserveconstraints.biblioitemnumber = ?
1860 AND reserves.borrowernumber = reserveconstraints.borrowernumber
1861 AND reserves.reservedate = reserveconstraints.reservedate )
1862 OR reserves.constrainttype='a' )
1863 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1864 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1865 AND suspend = 0
1867 $sth = $dbh->prepare($query);
1868 $sth->execute( $biblio, $bibitem, $itemnumber, $lookahead||0);
1869 @results = ();
1870 while ( my $data = $sth->fetchrow_hashref ) {
1871 push( @results, $data );
1873 return @results;
1876 =head2 _koha_notify_reserve
1878 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1880 Sends a notification to the patron that their hold has been filled (through
1881 ModReserveAffect, _not_ ModReserveFill)
1883 =cut
1885 sub _koha_notify_reserve {
1886 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1888 my $dbh = C4::Context->dbh;
1889 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1891 # Try to get the borrower's email address
1892 my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1894 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1895 borrowernumber => $borrowernumber,
1896 message_name => 'Hold_Filled'
1897 } );
1899 my $sth = $dbh->prepare("
1900 SELECT *
1901 FROM reserves
1902 WHERE borrowernumber = ?
1903 AND biblionumber = ?
1905 $sth->execute( $borrowernumber, $biblionumber );
1906 my $reserve = $sth->fetchrow_hashref;
1907 my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1909 my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1911 my %letter_params = (
1912 module => 'reserves',
1913 branchcode => $reserve->{branchcode},
1914 tables => {
1915 'branches' => $branch_details,
1916 'borrowers' => $borrower,
1917 'biblio' => $biblionumber,
1918 'reserves' => $reserve,
1919 'items', $reserve->{'itemnumber'},
1921 substitute => { today => C4::Dates->new()->output() },
1924 my $notification_sent = 0; #Keeping track if a Hold_filled message is sent. If no message can be sent, then default to a print message.
1925 my $send_notification = sub {
1926 my ( $mtt, $letter_code ) = (@_);
1927 return unless defined $letter_code;
1928 $letter_params{letter_code} = $letter_code;
1929 $letter_params{message_transport_type} = $mtt;
1930 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1931 unless ($letter) {
1932 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1933 return;
1936 C4::Letters::EnqueueLetter( {
1937 letter => $letter,
1938 borrowernumber => $borrowernumber,
1939 from_address => $admin_email_address,
1940 message_transport_type => $mtt,
1941 } );
1944 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1945 if ( ($mtt eq 'email' and not $to_address) or ($mtt eq 'sms' and not $borrower->{smsalertnumber}) ) {
1946 # email or sms is requested but not exist
1947 next;
1949 &$send_notification($mtt, $letter_code);
1950 $notification_sent++;
1952 #Making sure that a print notification is sent if no other transport types can be utilized.
1953 if (! $notification_sent) {
1954 &$send_notification('print', 'HOLD');
1959 =head2 _ShiftPriorityByDateAndPriority
1961 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1963 This increments the priority of all reserves after the one
1964 with either the lowest date after C<$reservedate>
1965 or the lowest priority after C<$priority>.
1967 It effectively makes room for a new reserve to be inserted with a certain
1968 priority, which is returned.
1970 This is most useful when the reservedate can be set by the user. It allows
1971 the new reserve to be placed before other reserves that have a later
1972 reservedate. Since priority also is set by the form in reserves/request.pl
1973 the sub accounts for that too.
1975 =cut
1977 sub _ShiftPriorityByDateAndPriority {
1978 my ( $biblio, $resdate, $new_priority ) = @_;
1980 my $dbh = C4::Context->dbh;
1981 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1982 my $sth = $dbh->prepare( $query );
1983 $sth->execute( $biblio, $resdate, $new_priority );
1984 my $min_priority = $sth->fetchrow;
1985 # if no such matches are found, $new_priority remains as original value
1986 $new_priority = $min_priority if ( $min_priority );
1988 # Shift the priority up by one; works in conjunction with the next SQL statement
1989 $query = "UPDATE reserves
1990 SET priority = priority+1
1991 WHERE biblionumber = ?
1992 AND borrowernumber = ?
1993 AND reservedate = ?
1994 AND found IS NULL";
1995 my $sth_update = $dbh->prepare( $query );
1997 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1998 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1999 $sth = $dbh->prepare( $query );
2000 $sth->execute( $new_priority, $biblio );
2001 while ( my $row = $sth->fetchrow_hashref ) {
2002 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2005 return $new_priority; # so the caller knows what priority they wind up receiving
2008 =head2 MoveReserve
2010 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2012 Use when checking out an item to handle reserves
2013 If $cancelreserve boolean is set to true, it will remove existing reserve
2015 =cut
2017 sub MoveReserve {
2018 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2020 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
2021 return unless $res;
2023 my $biblionumber = $res->{biblionumber};
2024 my $biblioitemnumber = $res->{biblioitemnumber};
2026 if ($res->{borrowernumber} == $borrowernumber) {
2027 ModReserveFill($res);
2029 else {
2030 # warn "Reserved";
2031 # The item is reserved by someone else.
2032 # Find this item in the reserves
2034 my $borr_res;
2035 foreach (@$all_reserves) {
2036 $_->{'borrowernumber'} == $borrowernumber or next;
2037 $_->{'biblionumber'} == $biblionumber or next;
2039 $borr_res = $_;
2040 last;
2043 if ( $borr_res ) {
2044 # The item is reserved by the current patron
2045 ModReserveFill($borr_res);
2048 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2049 RevertWaitingStatus({ itemnumber => $itemnumber });
2051 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2052 CancelReserve({
2053 biblionumber => $res->{'biblionumber'},
2054 itemnumber => $res->{'itemnumber'},
2055 borrowernumber => $res->{'borrowernumber'}
2061 =head2 MergeHolds
2063 MergeHolds($dbh,$to_biblio, $from_biblio);
2065 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2067 =cut
2069 sub MergeHolds {
2070 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2071 my $sth = $dbh->prepare(
2072 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2074 $sth->execute($from_biblio);
2075 if ( my $data = $sth->fetchrow_hashref() ) {
2077 # holds exist on old record, if not we don't need to do anything
2078 $sth = $dbh->prepare(
2079 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2080 $sth->execute( $to_biblio, $from_biblio );
2082 # Reorder by date
2083 # don't reorder those already waiting
2085 $sth = $dbh->prepare(
2086 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2088 my $upd_sth = $dbh->prepare(
2089 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2090 AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2092 $sth->execute( $to_biblio, 'W', 'T' );
2093 my $priority = 1;
2094 while ( my $reserve = $sth->fetchrow_hashref() ) {
2095 $upd_sth->execute(
2096 $priority, $to_biblio,
2097 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2098 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2100 $priority++;
2105 =head2 RevertWaitingStatus
2107 RevertWaitingStatus({ itemnumber => $itemnumber });
2109 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2111 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2112 item level hold, even if it was only a bibliolevel hold to
2113 begin with. This is because we can no longer know if a hold
2114 was item-level or bib-level after a hold has been set to
2115 waiting status.
2117 =cut
2119 sub RevertWaitingStatus {
2120 my ( $params ) = @_;
2121 my $itemnumber = $params->{'itemnumber'};
2123 return unless ( $itemnumber );
2125 my $dbh = C4::Context->dbh;
2127 ## Get the waiting reserve we want to revert
2128 my $query = "
2129 SELECT * FROM reserves
2130 WHERE itemnumber = ?
2131 AND found IS NOT NULL
2133 my $sth = $dbh->prepare( $query );
2134 $sth->execute( $itemnumber );
2135 my $reserve = $sth->fetchrow_hashref();
2137 ## Increment the priority of all other non-waiting
2138 ## reserves for this bib record
2139 $query = "
2140 UPDATE reserves
2142 priority = priority + 1
2143 WHERE
2144 biblionumber = ?
2146 priority > 0
2148 $sth = $dbh->prepare( $query );
2149 $sth->execute( $reserve->{'biblionumber'} );
2151 ## Fix up the currently waiting reserve
2152 $query = "
2153 UPDATE reserves
2155 priority = 1,
2156 found = NULL,
2157 waitingdate = NULL
2158 WHERE
2159 reserve_id = ?
2161 $sth = $dbh->prepare( $query );
2162 $sth->execute( $reserve->{'reserve_id'} );
2163 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2166 =head2 GetReserveId
2168 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2170 Returnes the first reserve id that matches the given criteria
2172 =cut
2174 sub GetReserveId {
2175 my ( $params ) = @_;
2177 return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2179 my $dbh = C4::Context->dbh();
2181 my $sql = "SELECT reserve_id FROM reserves WHERE ";
2183 my @params;
2184 my @limits;
2185 foreach my $key ( keys %$params ) {
2186 if ( defined( $params->{$key} ) ) {
2187 push( @limits, "$key = ?" );
2188 push( @params, $params->{$key} );
2192 $sql .= join( " AND ", @limits );
2194 my $sth = $dbh->prepare( $sql );
2195 $sth->execute( @params );
2196 my $row = $sth->fetchrow_hashref();
2198 return $row->{'reserve_id'};
2201 =head2 ReserveSlip
2203 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2205 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2207 =cut
2209 sub ReserveSlip {
2210 my ($branch, $borrowernumber, $biblionumber) = @_;
2212 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2214 my $reserve_id = GetReserveId({
2215 biblionumber => $biblionumber,
2216 borrowernumber => $borrowernumber
2217 }) or return;
2218 my $reserve = GetReserveInfo($reserve_id) or return;
2220 return C4::Letters::GetPreparedLetter (
2221 module => 'circulation',
2222 letter_code => 'RESERVESLIP',
2223 branchcode => $branch,
2224 tables => {
2225 'reserves' => $reserve,
2226 'branches' => $reserve->{branchcode},
2227 'borrowers' => $reserve->{borrowernumber},
2228 'biblio' => $reserve->{biblionumber},
2229 'items' => $reserve->{itemnumber},
2234 =head2 GetReservesControlBranch
2236 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2238 Return the branchcode to be used to determine which reserves
2239 policy applies to a transaction.
2241 C<$item> is a hashref for an item. Only 'homebranch' is used.
2243 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2245 =cut
2247 sub GetReservesControlBranch {
2248 my ( $item, $borrower ) = @_;
2250 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2252 my $branchcode =
2253 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2254 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2255 : undef;
2257 return $branchcode;
2260 =head2 CalculatePriority
2262 my $p = CalculatePriority($biblionumber, $resdate);
2264 Calculate priority for a new reserve on biblionumber, placing it at
2265 the end of the line of all holds whose start date falls before
2266 the current system time and that are neither on the hold shelf
2267 or in transit.
2269 The reserve date parameter is optional; if it is supplied, the
2270 priority is based on the set of holds whose start date falls before
2271 the parameter value.
2273 After calculation of this priority, it is recommended to call
2274 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2275 AddReserves.
2277 =cut
2279 sub CalculatePriority {
2280 my ( $biblionumber, $resdate ) = @_;
2282 my $sql = q{
2283 SELECT COUNT(*) FROM reserves
2284 WHERE biblionumber = ?
2285 AND priority > 0
2286 AND (found IS NULL OR found = '')
2288 #skip found==W or found==T (waiting or transit holds)
2289 if( $resdate ) {
2290 $sql.= ' AND ( reservedate <= ? )';
2292 else {
2293 $sql.= ' AND ( reservedate < NOW() )';
2295 my $dbh = C4::Context->dbh();
2296 my @row = $dbh->selectrow_array(
2297 $sql,
2298 undef,
2299 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2302 return @row ? $row[0]+1 : 1;
2305 =head1 AUTHOR
2307 Koha Development Team <http://koha-community.org/>
2309 =cut