3 # Copyright 2000-2002 Katipo Communications
4 # 2006 SAN Ouest Provence
5 # 2007-2010 BibLibre Paul POULAIN
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
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.
25 #use warnings; FIXME - Bug 2505
33 # for _koha_notify_reserve
34 use C4
::Members
::Messaging
;
37 use C4
::Branch
qw( GetBranchDetail );
38 use C4
::Dates
qw( format_date_in_iso );
42 use List
::MoreUtils
qw( firstidx );
44 use vars
qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
48 C4::Reserves - Koha functions for dealing with reservation.
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
90 # set the version for version checking
91 $VERSION = 3.07.00.049;
98 &GetReservesFromItemnumber
99 &GetReservesFromBiblionumber
100 &GetReservesFromBorrowernumber
101 &GetReservesForBranch
115 &ModReserveMinusPriority
122 &CancelExpiredReserves
124 &AutoUnsuspendReserves
126 &IsAvailableForItemLevelRequest
129 &ToggleLowestPriority
135 &GetReservesControlBranch
137 @EXPORT_OK = qw( MergeHolds );
142 AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
148 $branch, $borrowernumber, $biblionumber,
149 $constraint, $bibitems, $priority, $resdate, $expdate, $notes,
150 $title, $checkitem, $found
153 GetReserveFee
($borrowernumber, $biblionumber, $constraint,
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 );
160 $expdate = format_date_in_iso
( $expdate );
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 );
170 # If the reserv had the waiting status, we had the value of the resdate
171 if ( $found eq 'W' ) {
172 $waitingdate = $resdate;
176 # updates take place here
178 my $nextacctno = &getnextacctno
( $borrowernumber );
180 INSERT INTO accountlines
181 (borrowernumber
,accountno
,date
,amount
,description
,accounttype
,amountoutstanding
)
183 (?
,?
,now
(),?
,?
,'Res',?
)
185 my $usth = $dbh->prepare($query);
186 $usth->execute( $borrowernumber, $nextacctno, $fee,
187 "Reserve Charge - $title", $fee );
193 (borrowernumber
,biblionumber
,reservedate
,branchcode
,constrainttype
,
194 priority
,reservenotes
,itemnumber
,found
,waitingdate
,expirationdate
)
199 my $sth = $dbh->prepare($query);
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,
215 'branches' => $branch_details,
216 'borrowers' => $borrower,
217 'biblio' => $biblionumber,
218 'items' => $checkitem,
222 my $admin_email_address =$branch_details->{'branchemail'} || C4
::Context
->preference('KohaAdminEmailAddress');
224 C4
::Letters
::EnqueueLetter
(
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?
238 INSERT INTO reserveconstraints
239 (borrowernumber
,biblionumber
,reservedate
,biblioitemnumber
)
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?
253 $res = GetReserve( $reserve_id );
255 Return the current reserve.
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,
277 This function gets the list of reservations for one C<$biblionumber>,
278 returning an arrayref pointing to the reserves for C<$biblionumber>.
282 sub GetReservesFromBiblionumber
{
284 my $biblionumber = $params->{biblionumber
} or return [];
285 my $itemnumber = $params->{itemnumber
};
286 my $all_dates = $params->{all_dates
} // 0;
287 my $dbh = C4
::Context
->dbh;
289 # Find the desired items in the reserves
294 timestamp AS rtimestamp,
308 WHERE biblionumber = ? ";
309 push( @params, $biblionumber );
310 unless ( $all_dates ) {
311 $query .= " AND reservedate <= CAST(NOW() AS DATE) ";
314 $query .= " AND ( itemnumber IS NULL OR itemnumber = ? )";
315 push( @params, $itemnumber );
317 $query .= "ORDER BY priority";
318 my $sth = $dbh->prepare($query);
319 $sth->execute( @params );
322 while ( my $data = $sth->fetchrow_hashref ) {
324 # FIXME - What is this doing? How do constraints work?
325 if ($data->{constrainttype
} eq 'o') {
327 SELECT biblioitemnumber
328 FROM reserveconstraints
329 WHERE biblionumber = ?
330 AND borrowernumber = ?
333 my $csth = $dbh->prepare($query);
334 $csth->execute($data->{biblionumber
}, $data->{borrowernumber
}, $data->{reservedate
});
336 while ( my $bibitemnos = $csth->fetchrow_array ) {
337 push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref
339 my $count = scalar @bibitemno;
341 # if we have two or more different specific itemtypes
342 # reserved by same person on same day
345 $bdata = GetBiblioItemData
( $bibitemno[$i] ); # FIXME: This doesn't make sense.
346 $i++; # $i can increase each pass, but the next @bibitemno might be smaller?
349 # Look up the book we just found.
350 $bdata = GetBiblioItemData
( $bibitemno[0] );
352 # Add the results of this latest search to the current
354 # FIXME - An 'each' would probably be more efficient.
355 foreach my $key ( keys %$bdata ) {
356 $data->{$key} = $bdata->{$key};
359 push @results, $data;
364 =head2 GetReservesFromItemnumber
366 ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $waitingdate ) = GetReservesFromItemnumber($itemnumber);
368 Get the first reserve for a specific item number (based on priority). Returns the abovementioned values for that reserve.
370 The routine does not look at future reserves (read: item level holds), but DOES include future waits (a confirmed future hold).
374 sub GetReservesFromItemnumber
{
375 my ( $itemnumber ) = @_;
376 my $dbh = C4
::Context
->dbh;
378 SELECT reservedate,borrowernumber,branchcode,reserve_id,waitingdate
380 WHERE itemnumber=? AND ( reservedate <= CAST(now() AS date) OR
381 waitingdate IS NOT NULL )
384 my $sth_res = $dbh->prepare($query);
385 $sth_res->execute($itemnumber);
386 my ( $reservedate, $borrowernumber,$branchcode, $reserve_id, $wait ) = $sth_res->fetchrow_array;
387 return ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $wait );
390 =head2 GetReservesFromBorrowernumber
392 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
398 sub GetReservesFromBorrowernumber
{
399 my ( $borrowernumber, $status ) = @_;
400 my $dbh = C4
::Context
->dbh;
403 $sth = $dbh->prepare("
406 WHERE borrowernumber=?
410 $sth->execute($borrowernumber,$status);
412 $sth = $dbh->prepare("
415 WHERE borrowernumber=?
418 $sth->execute($borrowernumber);
420 my $data = $sth->fetchall_arrayref({});
423 #-------------------------------------------------------------------------------------
424 =head2 CanBookBeReserved
426 $error = &CanBookBeReserved($borrowernumber, $biblionumber)
430 sub CanBookBeReserved
{
431 my ($borrowernumber, $biblionumber) = @_;
433 my $items = GetItemnumbersForBiblio
($biblionumber);
434 #get items linked via host records
435 my @hostitems = get_hostitemnumbers_of
($biblionumber);
437 push (@
$items,@hostitems);
440 foreach my $item (@
$items){
441 return 1 if CanItemBeReserved
($borrowernumber, $item);
446 =head2 CanItemBeReserved
448 $error = &CanItemBeReserved($borrowernumber, $itemnumber)
450 This function return 1 if an item can be issued by this borrower.
454 sub CanItemBeReserved
{
455 my ($borrowernumber, $itemnumber) = @_;
457 my $dbh = C4
::Context
->dbh;
458 my $allowedreserves = 0;
460 my $controlbranch = C4
::Context
->preference('ReservesControlBranch');
461 my $itype = C4
::Context
->preference('item-level_itypes') ?
"itype" : "itemtype";
463 # we retrieve borrowers and items informations #
464 my $item = GetItem
($itemnumber);
465 my $borrower = C4
::Members
::GetMember
('borrowernumber'=>$borrowernumber);
467 # we retrieve user rights on this itemtype and branchcode
468 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
470 WHERE (categorycode in (?,'*') )
471 AND (itemtype IN (?,'*'))
472 AND (branchcode IN (?,'*'))
479 my $querycount ="SELECT
482 LEFT JOIN items USING (itemnumber)
483 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
484 LEFT JOIN borrowers USING (borrowernumber)
485 WHERE borrowernumber = ?
489 my $itemtype = $item->{$itype};
490 my $categorycode = $borrower->{categorycode
};
492 my $branchfield = "reserves.branchcode";
494 if( $controlbranch eq "ItemHomeLibrary" ){
495 $branchfield = "items.homebranch";
496 $branchcode = $item->{homebranch
};
497 }elsif( $controlbranch eq "PatronLibrary" ){
498 $branchfield = "borrowers.branchcode";
499 $branchcode = $borrower->{branchcode
};
503 $sth->execute($categorycode, $itemtype, $branchcode);
504 if(my $rights = $sth->fetchrow_hashref()){
505 $itemtype = $rights->{itemtype
};
506 $allowedreserves = $rights->{reservesallowed
};
513 $querycount .= "AND $branchfield = ?";
515 $querycount .= " AND $itype = ?" if ($itemtype ne "*");
516 my $sthcount = $dbh->prepare($querycount);
518 if($itemtype eq "*"){
519 $sthcount->execute($borrowernumber, $branchcode);
521 $sthcount->execute($borrowernumber, $branchcode, $itemtype);
524 my $reservecount = "0";
525 if(my $rowcount = $sthcount->fetchrow_hashref()){
526 $reservecount = $rowcount->{count
};
529 # we check if it's ok or not
530 if( $reservecount >= $allowedreserves ){
534 # If reservecount is ok, we check item branch if IndependentBranches is ON
535 # and canreservefromotherbranches is OFF
536 if ( C4
::Context
->preference('IndependentBranches')
537 and !C4
::Context
->preference('canreservefromotherbranches') )
539 my $itembranch = $item->{homebranch
};
540 if ($itembranch ne $borrower->{branchcode
}) {
547 #--------------------------------------------------------------------------------
548 =head2 GetReserveCount
550 $number = &GetReserveCount($borrowernumber);
552 this function returns the number of reservation for a borrower given on input arg.
556 sub GetReserveCount
{
557 my ($borrowernumber) = @_;
559 my $dbh = C4
::Context
->dbh;
562 SELECT COUNT(*) AS counter
564 WHERE borrowernumber = ?
566 my $sth = $dbh->prepare($query);
567 $sth->execute($borrowernumber);
568 my $row = $sth->fetchrow_hashref;
569 return $row->{counter
};
572 =head2 GetOtherReserves
574 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
576 Check queued list of this document and check if this document must be transfered
580 sub GetOtherReserves
{
581 my ($itemnumber) = @_;
584 my ( undef, $checkreserves, undef ) = CheckReserves
($itemnumber);
585 if ($checkreserves) {
586 my $iteminfo = GetItem
($itemnumber);
587 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
588 $messages->{'transfert'} = $checkreserves->{'branchcode'};
589 #minus priorities of others reservs
590 ModReserveMinusPriority
(
592 $checkreserves->{'reserve_id'},
595 #launch the subroutine dotransfer
596 C4
::Items
::ModItemTransfer
(
598 $iteminfo->{'holdingbranch'},
599 $checkreserves->{'branchcode'}
604 #step 2b : case of a reservation on the same branch, set the waiting status
606 $messages->{'waiting'} = 1;
607 ModReserveMinusPriority
(
609 $checkreserves->{'reserve_id'},
611 ModReserveStatus
($itemnumber,'W');
614 $nextreservinfo = $checkreserves->{'borrowernumber'};
617 return ( $messages, $nextreservinfo );
622 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
624 Calculate the fee for a reserve
629 my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
632 my $dbh = C4
::Context
->dbh;
633 my $const = lc substr( $constraint, 0, 1 );
635 SELECT
* FROM borrowers
636 LEFT JOIN categories ON borrowers
.categorycode
= categories
.categorycode
637 WHERE borrowernumber
= ?
639 my $sth = $dbh->prepare($query);
640 $sth->execute($borrowernumber);
641 my $data = $sth->fetchrow_hashref;
642 my $fee = $data->{'reservefee'};
643 my $cntitems = @
- > $bibitems;
647 # check for items on issue
648 # first find biblioitem records
650 my $sth1 = $dbh->prepare(
651 "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
652 WHERE (biblio.biblionumber = ?)"
654 $sth1->execute($biblionumber);
655 while ( my $data1 = $sth1->fetchrow_hashref ) {
656 if ( $const eq "a" ) {
657 push @biblioitems, $data1;
662 while ( $x < $cntitems ) {
663 if ( @
$bibitems->{'biblioitemnumber'} ==
664 $data->{'biblioitemnumber'} )
670 if ( $const eq 'o' ) {
672 push @biblioitems, $data1;
677 push @biblioitems, $data1;
682 my $cntitemsfound = @biblioitems;
686 while ( $x < $cntitemsfound ) {
687 my $bitdata = $biblioitems[$x];
688 my $sth2 = $dbh->prepare(
690 WHERE biblioitemnumber = ?"
692 $sth2->execute( $bitdata->{'biblioitemnumber'} );
693 while ( my $itdata = $sth2->fetchrow_hashref ) {
694 my $sth3 = $dbh->prepare(
695 "SELECT * FROM issues
696 WHERE itemnumber = ?"
698 $sth3->execute( $itdata->{'itemnumber'} );
699 if ( my $isdata = $sth3->fetchrow_hashref ) {
707 if ( $allissued == 0 ) {
709 $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
710 $rsth->execute($biblionumber);
711 if ( my $rdata = $rsth->fetchrow_hashref ) {
721 =head2 GetReservesToBranch
723 @transreserv = GetReservesToBranch( $frombranch );
725 Get reserve list for a given branch
729 sub GetReservesToBranch
{
730 my ( $frombranch ) = @_;
731 my $dbh = C4
::Context
->dbh;
732 my $sth = $dbh->prepare(
733 "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
738 $sth->execute( $frombranch );
741 while ( my $data = $sth->fetchrow_hashref ) {
742 $transreserv[$i] = $data;
745 return (@transreserv);
748 =head2 GetReservesForBranch
750 @transreserv = GetReservesForBranch($frombranch);
754 sub GetReservesForBranch
{
755 my ($frombranch) = @_;
756 my $dbh = C4
::Context
->dbh;
759 SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
764 $query .= " AND branchcode=? " if ( $frombranch );
765 $query .= "ORDER BY waitingdate" ;
767 my $sth = $dbh->prepare($query);
769 $sth->execute($frombranch);
776 while ( my $data = $sth->fetchrow_hashref ) {
777 $transreserv[$i] = $data;
780 return (@transreserv);
783 =head2 GetReserveStatus
785 $reservestatus = GetReserveStatus($itemnumber, $biblionumber);
787 Take an itemnumber or a biblionumber and return the status of the reserve places on it.
788 If several reserves exist, the reserve with the lower priority is given.
792 ## FIXME: I don't think this does what it thinks it does.
793 ## It only ever checks the first reserve result, even though
794 ## multiple reserves for that bib can have the itemnumber set
795 ## the sub is only used once in the codebase.
796 sub GetReserveStatus
{
797 my ($itemnumber, $biblionumber) = @_;
799 my $dbh = C4
::Context
->dbh;
801 my ($sth, $found, $priority);
803 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
804 $sth->execute($itemnumber);
805 ($found, $priority) = $sth->fetchrow_array;
808 if ( $biblionumber and not defined $found and not defined $priority ) {
809 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE biblionumber = ? order by priority LIMIT 1");
810 $sth->execute($biblionumber);
811 ($found, $priority) = $sth->fetchrow_array;
815 return 'Waiting' if $found eq 'W' and $priority == 0;
816 return 'Finished' if $found eq 'F';
819 return 'Reserved' if $priority > 0;
821 return ''; # empty string here will remove need for checking undef, or less log lines
826 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
827 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
828 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
830 Find a book in the reserves.
832 C<$itemnumber> is the book's item number.
833 C<$lookahead> is the number of days to look in advance for future reserves.
835 As I understand it, C<&CheckReserves> looks for the given item in the
836 reserves. If it is found, that's a match, and C<$status> is set to
839 Otherwise, it finds the most important item in the reserves with the
840 same biblio number as this book (I'm not clear on this) and returns it
841 with C<$status> set to C<Reserved>.
843 C<&CheckReserves> returns a two-element list:
845 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
847 C<$reserve> is the reserve item that matched. It is a
848 reference-to-hash whose keys are mostly the fields of the reserves
849 table in the Koha database.
854 my ( $item, $barcode, $lookahead_days) = @_;
855 my $dbh = C4
::Context
->dbh;
858 if (C4
::Context
->preference('item-level_itypes')){
860 SELECT items.biblionumber,
861 items.biblioitemnumber,
862 itemtypes.notforloan,
863 items.notforloan AS itemnotforloan,
866 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
867 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
872 SELECT items.biblionumber,
873 items.biblioitemnumber,
874 itemtypes.notforloan,
875 items.notforloan AS itemnotforloan,
878 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
879 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
884 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
885 $sth->execute($item);
888 $sth = $dbh->prepare("$select WHERE barcode = ?");
889 $sth->execute($barcode);
891 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
892 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
894 return ( '' ) unless $itemnumber; # bail if we got nothing.
896 # if item is not for loan it cannot be reserved either.....
897 # execpt where items.notforloan < 0 : This indicates the item is holdable.
898 return ( '' ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
900 # Find this item in the reserves
901 my @reserves = _Findgroupreserve
( $bibitem, $biblio, $itemnumber, $lookahead_days);
903 # $priority and $highest are used to find the most important item
904 # in the list returned by &_Findgroupreserve. (The lower $priority,
905 # the more important the item.)
906 # $highest is the most important item we've seen so far.
908 if (scalar @reserves) {
909 my $priority = 10000000;
910 foreach my $res (@reserves) {
911 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
912 return ( "Waiting", $res, \
@reserves ); # Found it
914 # See if this item is more important than what we've got so far
915 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
916 my $borrowerinfo=C4
::Members
::GetMember
(borrowernumber
=> $res->{'borrowernumber'});
917 my $iteminfo=C4
::Items
::GetItem
($itemnumber);
918 my $branch = GetReservesControlBranch
( $iteminfo, $borrowerinfo );
919 my $branchitemrule = C4
::Circulation
::GetBranchItemRule
($branch,$iteminfo->{'itype'});
920 next if ($branchitemrule->{'holdallowed'} == 0);
921 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
922 $priority = $res->{'priority'};
929 # If we get this far, then no exact match was found.
930 # We return the most important (i.e. next) reservation.
932 $highest->{'itemnumber'} = $item;
933 return ( "Reserved", $highest, \
@reserves );
939 =head2 CancelExpiredReserves
941 CancelExpiredReserves();
943 Cancels all reserves with an expiration date from before today.
947 sub CancelExpiredReserves
{
949 # Cancel reserves that have passed their expiration date.
950 my $dbh = C4
::Context
->dbh;
951 my $sth = $dbh->prepare( "
952 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
953 AND expirationdate IS NOT NULL
958 while ( my $res = $sth->fetchrow_hashref() ) {
959 CancelReserve
({ reserve_id
=> $res->{'reserve_id'} });
962 # Cancel reserves that have been waiting too long
963 if ( C4
::Context
->preference("ExpireReservesMaxPickUpDelay") ) {
964 my $max_pickup_delay = C4
::Context
->preference("ReservesMaxPickUpDelay");
965 my $charge = C4
::Context
->preference("ExpireReservesMaxPickUpDelayCharge");
967 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
968 $sth = $dbh->prepare( $query );
969 $sth->execute( $max_pickup_delay );
971 while (my $res = $sth->fetchrow_hashref ) {
973 manualinvoice
($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
976 CancelReserve
({ reserve_id
=> $res->{'reserve_id'} });
982 =head2 AutoUnsuspendReserves
984 AutoUnsuspendReserves();
986 Unsuspends all suspended reserves with a suspend_until date from before today.
990 sub AutoUnsuspendReserves
{
992 my $dbh = C4
::Context
->dbh;
994 my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
995 my $sth = $dbh->prepare( $query );
1000 =head2 CancelReserve
1002 CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber ] });
1009 my ( $params ) = @_;
1011 my $reserve_id = $params->{'reserve_id'};
1012 $reserve_id = GetReserveId
( $params ) unless ( $reserve_id );
1014 return unless ( $reserve_id );
1016 my $dbh = C4
::Context
->dbh;
1018 my $reserve = GetReserve
( $reserve_id );
1022 SET cancellationdate = now(),
1025 WHERE reserve_id = ?
1027 my $sth = $dbh->prepare($query);
1028 $sth->execute( $reserve_id );
1031 INSERT INTO old_reserves
1032 SELECT * FROM reserves
1033 WHERE reserve_id = ?
1035 $sth = $dbh->prepare($query);
1036 $sth->execute( $reserve_id );
1039 DELETE FROM reserves
1040 WHERE reserve_id = ?
1042 $sth = $dbh->prepare($query);
1043 $sth->execute( $reserve_id );
1045 # now fix the priority on the others....
1046 _FixPriority
({ biblionumber
=> $reserve->{biblionumber
} });
1051 ModReserve({ rank => $rank,
1052 reserve_id => $reserve_id,
1053 branchcode => $branchcode
1054 [, itemnumber => $itemnumber ]
1055 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1058 Change a hold request's priority or cancel it.
1060 C<$rank> specifies the effect of the change. If C<$rank>
1061 is 'W' or 'n', nothing happens. This corresponds to leaving a
1062 request alone when changing its priority in the holds queue
1065 If C<$rank> is 'del', the hold request is cancelled.
1067 If C<$rank> is an integer greater than zero, the priority of
1068 the request is set to that value. Since priority != 0 means
1069 that the item is not waiting on the hold shelf, setting the
1070 priority to a non-zero value also sets the request's found
1071 status and waiting date to NULL.
1073 The optional C<$itemnumber> parameter is used only when
1074 C<$rank> is a non-zero integer; if supplied, the itemnumber
1075 of the hold request is set accordingly; if omitted, the itemnumber
1078 B<FIXME:> Note that the forgoing can have the effect of causing
1079 item-level hold requests to turn into title-level requests. This
1080 will be fixed once reserves has separate columns for requested
1081 itemnumber and supplying itemnumber.
1086 my ( $params ) = @_;
1088 my $rank = $params->{'rank'};
1089 my $reserve_id = $params->{'reserve_id'};
1090 my $branchcode = $params->{'branchcode'};
1091 my $itemnumber = $params->{'itemnumber'};
1092 my $suspend_until = $params->{'suspend_until'};
1093 my $borrowernumber = $params->{'borrowernumber'};
1094 my $biblionumber = $params->{'biblionumber'};
1096 return if $rank eq "W";
1097 return if $rank eq "n";
1099 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1100 $reserve_id = GetReserveId
({ biblionumber
=> $biblionumber, borrowernumber
=> $borrowernumber, itemnumber
=> $itemnumber }) unless ( $reserve_id );
1102 my $dbh = C4
::Context
->dbh;
1103 if ( $rank eq "del" ) {
1104 CancelReserve
({ reserve_id
=> $reserve_id });
1106 elsif ($rank =~ /^\d+/ and $rank > 0) {
1108 UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1109 WHERE reserve_id = ?
1111 my $sth = $dbh->prepare($query);
1112 $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
1114 if ( defined( $suspend_until ) ) {
1115 if ( $suspend_until ) {
1116 $suspend_until = C4
::Dates
->new( $suspend_until )->output("iso");
1117 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
1119 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
1123 _FixPriority
({ reserve_id
=> $reserve_id, rank
=>$rank });
1127 =head2 ModReserveFill
1129 &ModReserveFill($reserve);
1131 Fill a reserve. If I understand this correctly, this means that the
1132 reserved book has been found and given to the patron who reserved it.
1134 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1135 whose keys are fields from the reserves table in the Koha database.
1139 sub ModReserveFill
{
1141 my $dbh = C4
::Context
->dbh;
1142 # fill in a reserve record....
1143 my $reserve_id = $res->{'reserve_id'};
1144 my $biblionumber = $res->{'biblionumber'};
1145 my $borrowernumber = $res->{'borrowernumber'};
1146 my $resdate = $res->{'reservedate'};
1148 # get the priority on this record....
1150 my $query = "SELECT priority
1152 WHERE biblionumber = ?
1153 AND borrowernumber = ?
1154 AND reservedate = ?";
1155 my $sth = $dbh->prepare($query);
1156 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1157 ($priority) = $sth->fetchrow_array;
1159 # update the database...
1160 $query = "UPDATE reserves
1163 WHERE biblionumber = ?
1165 AND borrowernumber = ?
1167 $sth = $dbh->prepare($query);
1168 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1170 # move to old_reserves
1171 $query = "INSERT INTO old_reserves
1172 SELECT * FROM reserves
1173 WHERE biblionumber = ?
1175 AND borrowernumber = ?
1177 $sth = $dbh->prepare($query);
1178 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1179 $query = "DELETE FROM reserves
1180 WHERE biblionumber = ?
1182 AND borrowernumber = ?
1184 $sth = $dbh->prepare($query);
1185 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1187 # now fix the priority on the others (if the priority wasn't
1188 # already sorted!)....
1189 unless ( $priority == 0 ) {
1190 _FixPriority
({ reserve_id
=> $reserve_id });
1194 =head2 ModReserveStatus
1196 &ModReserveStatus($itemnumber, $newstatus);
1198 Update the reserve status for the active (priority=0) reserve.
1200 $itemnumber is the itemnumber the reserve is on
1202 $newstatus is the new status.
1206 sub ModReserveStatus
{
1208 #first : check if we have a reservation for this item .
1209 my ($itemnumber, $newstatus) = @_;
1210 my $dbh = C4
::Context
->dbh;
1212 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1213 my $sth_set = $dbh->prepare($query);
1214 $sth_set->execute( $newstatus, $itemnumber );
1216 if ( C4
::Context
->preference("ReturnToShelvingCart") && $newstatus ) {
1217 CartToShelf
( $itemnumber );
1221 =head2 ModReserveAffect
1223 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1225 This function affect an item and a status for a given reserve
1226 The itemnumber parameter is used to find the biblionumber.
1227 with the biblionumber & the borrowernumber, we can affect the itemnumber
1228 to the correct reserve.
1230 if $transferToDo is not set, then the status is set to "Waiting" as well.
1231 otherwise, a transfer is on the way, and the end of the transfer will
1232 take care of the waiting status
1236 sub ModReserveAffect
{
1237 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1238 my $dbh = C4
::Context
->dbh;
1240 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1241 # attached to $itemnumber
1242 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1243 $sth->execute($itemnumber);
1244 my ($biblionumber) = $sth->fetchrow;
1246 # get request - need to find out if item is already
1247 # waiting in order to not send duplicate hold filled notifications
1248 my $reserve_id = GetReserveId
({
1249 borrowernumber
=> $borrowernumber,
1250 biblionumber
=> $biblionumber,
1252 return unless defined $reserve_id;
1253 my $request = GetReserveInfo
($reserve_id);
1254 my $already_on_shelf = ($request && $request->{found
} eq 'W') ?
1 : 0;
1256 # If we affect a reserve that has to be transfered, don't set to Waiting
1258 if ($transferToDo) {
1264 WHERE borrowernumber = ?
1265 AND biblionumber = ?
1269 # affect the reserve to Waiting as well.
1274 waitingdate = NOW(),
1276 WHERE borrowernumber = ?
1277 AND biblionumber = ?
1280 $sth = $dbh->prepare($query);
1281 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1282 _koha_notify_reserve
( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1284 if ( C4
::Context
->preference("ReturnToShelvingCart") ) {
1285 CartToShelf
( $itemnumber );
1291 =head2 ModReserveCancelAll
1293 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1295 function to cancel reserv,check other reserves, and transfer document if it's necessary
1299 sub ModReserveCancelAll
{
1302 my ( $itemnumber, $borrowernumber ) = @_;
1304 #step 1 : cancel the reservation
1305 my $CancelReserve = CancelReserve
({ itemnumber
=> $itemnumber, borrowernumber
=> $borrowernumber });
1307 #step 2 launch the subroutine of the others reserves
1308 ( $messages, $nextreservinfo ) = GetOtherReserves
($itemnumber);
1310 return ( $messages, $nextreservinfo );
1313 =head2 ModReserveMinusPriority
1315 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1317 Reduce the values of queued list
1321 sub ModReserveMinusPriority
{
1322 my ( $itemnumber, $reserve_id ) = @_;
1324 #first step update the value of the first person on reserv
1325 my $dbh = C4
::Context
->dbh;
1328 SET priority = 0 , itemnumber = ?
1329 WHERE reserve_id = ?
1331 my $sth_upd = $dbh->prepare($query);
1332 $sth_upd->execute( $itemnumber, $reserve_id );
1333 # second step update all others reserves
1334 _FixPriority
({ reserve_id
=> $reserve_id, rank
=> '0' });
1337 =head2 GetReserveInfo
1339 &GetReserveInfo($reserve_id);
1341 Get item and borrower details for a current hold.
1342 Current implementation this query should have a single result.
1346 sub GetReserveInfo
{
1347 my ( $reserve_id ) = @_;
1348 my $dbh = C4
::Context
->dbh;
1353 reserves.borrowernumber,
1354 reserves.biblionumber,
1355 reserves.branchcode,
1356 reserves.waitingdate,
1372 items.holdingbranch,
1373 items.itemcallnumber,
1379 LEFT JOIN items USING(itemnumber)
1380 LEFT JOIN borrowers USING(borrowernumber)
1381 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1382 WHERE reserves.reserve_id = ?";
1383 my $sth = $dbh->prepare($strsth);
1384 $sth->execute($reserve_id);
1386 my $data = $sth->fetchrow_hashref;
1390 =head2 IsAvailableForItemLevelRequest
1392 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1394 Checks whether a given item record is available for an
1395 item-level hold request. An item is available if
1397 * it is not lost AND
1398 * it is not damaged AND
1399 * it is not withdrawn AND
1400 * does not have a not for loan value > 0
1402 Whether or not the item is currently on loan is
1403 also checked - if the AllowOnShelfHolds system preference
1404 is ON, an item can be requested even if it is currently
1405 on loan to somebody else. If the system preference
1406 is OFF, an item that is currently checked out cannot
1407 be the target of an item-level hold request.
1409 Note that IsAvailableForItemLevelRequest() does not
1410 check if the staff operator is authorized to place
1411 a request on the item - in particular,
1412 this routine does not check IndependentBranches
1413 and canreservefromotherbranches.
1417 sub IsAvailableForItemLevelRequest
{
1418 my $itemnumber = shift;
1420 my $item = GetItem
($itemnumber);
1422 # must check the notforloan setting of the itemtype
1423 # FIXME - a lot of places in the code do this
1424 # or something similar - need to be
1426 my $dbh = C4
::Context
->dbh;
1427 my $notforloan_query;
1428 if (C4
::Context
->preference('item-level_itypes')) {
1429 $notforloan_query = "SELECT itemtypes.notforloan
1431 JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1432 WHERE itemnumber = ?";
1434 $notforloan_query = "SELECT itemtypes.notforloan
1436 JOIN biblioitems USING (biblioitemnumber)
1437 JOIN itemtypes USING (itemtype)
1438 WHERE itemnumber = ?";
1440 my $sth = $dbh->prepare($notforloan_query);
1441 $sth->execute($itemnumber);
1442 my $notforloan_per_itemtype = 0;
1443 if (my ($notforloan) = $sth->fetchrow_array) {
1444 $notforloan_per_itemtype = 1 if $notforloan;
1447 my $available_per_item = 1;
1448 $available_per_item = 0 if $item->{itemlost
} or
1449 ( $item->{notforloan
} > 0 ) or
1450 ($item->{damaged
} and not C4
::Context
->preference('AllowHoldsOnDamagedItems')) or
1451 $item->{withdrawn
} or
1452 $notforloan_per_itemtype;
1455 if (C4
::Context
->preference('AllowOnShelfHolds')) {
1456 return $available_per_item;
1458 return ($available_per_item and ($item->{onloan
} or GetReserveStatus
($itemnumber) eq "Waiting"));
1462 =head2 AlterPriority
1464 AlterPriority( $where, $reserve_id );
1466 This function changes a reserve's priority up, down, to the top, or to the bottom.
1467 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1472 my ( $where, $reserve_id ) = @_;
1474 my $dbh = C4
::Context
->dbh;
1476 my $reserve = GetReserve
( $reserve_id );
1478 if ( $reserve->{cancellationdate
} ) {
1479 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate
}.')';
1483 if ( $where eq 'up' || $where eq 'down' ) {
1485 my $priority = $reserve->{'priority'};
1486 $priority = $where eq 'up' ?
$priority - 1 : $priority + 1;
1487 _FixPriority
({ reserve_id
=> $reserve_id, rank
=> $priority })
1489 } elsif ( $where eq 'top' ) {
1491 _FixPriority
({ reserve_id
=> $reserve_id, rank
=> '1' })
1493 } elsif ( $where eq 'bottom' ) {
1495 _FixPriority
({ reserve_id
=> $reserve_id, rank
=> '999999' });
1500 =head2 ToggleLowestPriority
1502 ToggleLowestPriority( $borrowernumber, $biblionumber );
1504 This function sets the lowestPriority field to true if is false, and false if it is true.
1508 sub ToggleLowestPriority
{
1509 my ( $reserve_id ) = @_;
1511 my $dbh = C4
::Context
->dbh;
1513 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1514 $sth->execute( $reserve_id );
1516 _FixPriority
({ reserve_id
=> $reserve_id, rank
=> '999999' });
1519 =head2 ToggleSuspend
1521 ToggleSuspend( $reserve_id );
1523 This function sets the suspend field to true if is false, and false if it is true.
1524 If the reserve is currently suspended with a suspend_until date, that date will
1525 be cleared when it is unsuspended.
1530 my ( $reserve_id, $suspend_until ) = @_;
1532 $suspend_until = output_pref
({ dt
=> dt_from_string
( $suspend_until ), dateformat
=> 'iso' }) if ( $suspend_until );
1534 my $do_until = ( $suspend_until ) ?
'?' : 'NULL';
1536 my $dbh = C4
::Context
->dbh;
1538 my $sth = $dbh->prepare(
1539 "UPDATE reserves SET suspend = NOT suspend,
1540 suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1541 WHERE reserve_id = ?
1545 push( @params, $suspend_until ) if ( $suspend_until );
1546 push( @params, $reserve_id );
1548 $sth->execute( @params );
1554 borrowernumber => $borrowernumber,
1555 [ biblionumber => $biblionumber, ]
1556 [ suspend_until => $suspend_until, ]
1557 [ suspend => $suspend ]
1560 This function accepts a set of hash keys as its parameters.
1561 It requires either borrowernumber or biblionumber, or both.
1563 suspend_until is wholly optional.
1570 my $borrowernumber = $params{'borrowernumber'} || undef;
1571 my $biblionumber = $params{'biblionumber'} || undef;
1572 my $suspend_until = $params{'suspend_until'} || undef;
1573 my $suspend = defined( $params{'suspend'} ) ?
$params{'suspend'} : 1;
1575 $suspend_until = C4
::Dates
->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1577 return unless ( $borrowernumber || $biblionumber );
1579 my ( $query, $sth, $dbh, @query_params );
1581 $query = "UPDATE reserves SET suspend = ? ";
1582 push( @query_params, $suspend );
1584 $query .= ", suspend_until = NULL ";
1585 } elsif ( $suspend_until ) {
1586 $query .= ", suspend_until = ? ";
1587 push( @query_params, $suspend_until );
1589 $query .= " WHERE ";
1590 if ( $borrowernumber ) {
1591 $query .= " borrowernumber = ? ";
1592 push( @query_params, $borrowernumber );
1594 $query .= " AND " if ( $borrowernumber && $biblionumber );
1595 if ( $biblionumber ) {
1596 $query .= " biblionumber = ? ";
1597 push( @query_params, $biblionumber );
1599 $query .= " AND found IS NULL ";
1601 $dbh = C4
::Context
->dbh;
1602 $sth = $dbh->prepare( $query );
1603 $sth->execute( @query_params );
1610 reserve_id => $reserve_id,
1612 [ignoreSetLowestRank => $ignoreSetLowestRank]
1617 _FixPriority({ biblionumber => $biblionumber});
1619 This routine adjusts the priority of a hold request and holds
1622 In the first form, where a reserve_id is passed, the priority of the
1623 hold is set to supplied rank, and other holds for that bib are adjusted
1624 accordingly. If the rank is "del", the hold is cancelled. If no rank
1625 is supplied, all of the holds on that bib have their priority adjusted
1626 as if the second form had been used.
1628 In the second form, where a biblionumber is passed, the holds on that
1629 bib (that are not captured) are sorted in order of increasing priority,
1630 then have reserves.priority set so that the first non-captured hold
1631 has its priority set to 1, the second non-captured hold has its priority
1632 set to 2, and so forth.
1634 In both cases, holds that have the lowestPriority flag on are have their
1635 priority adjusted to ensure that they remain at the end of the line.
1637 Note that the ignoreSetLowestRank parameter is meant to be used only
1638 when _FixPriority calls itself.
1643 my ( $params ) = @_;
1644 my $reserve_id = $params->{reserve_id
};
1645 my $rank = $params->{rank
} // '';
1646 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank
};
1647 my $biblionumber = $params->{biblionumber
};
1649 my $dbh = C4
::Context
->dbh;
1651 unless ( $biblionumber ) {
1652 my $res = GetReserve
( $reserve_id );
1653 $biblionumber = $res->{biblionumber
};
1656 if ( $rank eq "del" ) {
1657 CancelReserve
({ reserve_id
=> $reserve_id });
1659 elsif ( $rank eq "W" || $rank eq "0" ) {
1661 # make sure priority for waiting or in-transit items is 0
1665 WHERE reserve_id = ?
1666 AND found IN ('W', 'T')
1668 my $sth = $dbh->prepare($query);
1669 $sth->execute( $reserve_id );
1675 SELECT reserve_id, borrowernumber, reservedate, constrainttype
1677 WHERE biblionumber = ?
1678 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1679 ORDER BY priority ASC
1681 my $sth = $dbh->prepare($query);
1682 $sth->execute( $biblionumber );
1683 while ( my $line = $sth->fetchrow_hashref ) {
1684 push( @priority, $line );
1687 # To find the matching index
1689 my $key = -1; # to allow for 0 to be a valid result
1690 for ( $i = 0 ; $i < @priority ; $i++ ) {
1691 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1692 $key = $i; # save the index
1697 # if index exists in array then move it to new position
1698 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1699 my $new_rank = $rank -
1700 1; # $new_rank is what you want the new index to be in the array
1701 my $moving_item = splice( @priority, $key, 1 );
1702 splice( @priority, $new_rank, 0, $moving_item );
1705 # now fix the priority on those that are left....
1709 WHERE reserve_id = ?
1711 $sth = $dbh->prepare($query);
1712 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1715 $priority[$j]->{'reserve_id'}
1719 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1722 unless ( $ignoreSetLowestRank ) {
1723 while ( my $res = $sth->fetchrow_hashref() ) {
1725 reserve_id
=> $res->{'reserve_id'},
1727 ignoreSetLowestRank
=> 1
1733 =head2 _Findgroupreserve
1735 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead);
1737 Looks for an item-specific match first, then for a title-level match, returning the
1738 first match found. If neither, then we look for a 3rd kind of match based on
1739 reserve constraints.
1740 Lookahead is the number of days to look in advance.
1742 TODO: add more explanation about reserve constraints
1744 C<&_Findgroupreserve> returns :
1745 C<@results> is an array of references-to-hash whose keys are mostly
1746 fields from the reserves table of the Koha database, plus
1747 C<biblioitemnumber>.
1751 sub _Findgroupreserve
{
1752 my ( $bibitem, $biblio, $itemnumber, $lookahead) = @_;
1753 my $dbh = C4
::Context
->dbh;
1755 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1756 # check for exact targetted match
1757 my $item_level_target_query = qq/
1758 SELECT reserves
.biblionumber AS biblionumber
,
1759 reserves
.borrowernumber AS borrowernumber
,
1760 reserves
.reservedate AS reservedate
,
1761 reserves
.branchcode AS branchcode
,
1762 reserves
.cancellationdate AS cancellationdate
,
1763 reserves
.found AS found
,
1764 reserves
.reservenotes AS reservenotes
,
1765 reserves
.priority AS priority
,
1766 reserves
.timestamp AS timestamp
,
1767 biblioitems
.biblioitemnumber AS biblioitemnumber
,
1768 reserves
.itemnumber AS itemnumber
,
1769 reserves
.reserve_id AS reserve_id
1771 JOIN biblioitems USING
(biblionumber
)
1772 JOIN hold_fill_targets USING
(biblionumber
, borrowernumber
, itemnumber
)
1775 AND item_level_request
= 1
1777 AND reservedate
<= DATE_ADD
(NOW
(),INTERVAL ? DAY
)
1780 my $sth = $dbh->prepare($item_level_target_query);
1781 $sth->execute($itemnumber, $lookahead||0);
1783 if ( my $data = $sth->fetchrow_hashref ) {
1784 push( @results, $data );
1786 return @results if @results;
1788 # check for title-level targetted match
1789 my $title_level_target_query = qq/
1790 SELECT reserves
.biblionumber AS biblionumber
,
1791 reserves
.borrowernumber AS borrowernumber
,
1792 reserves
.reservedate AS reservedate
,
1793 reserves
.branchcode AS branchcode
,
1794 reserves
.cancellationdate AS cancellationdate
,
1795 reserves
.found AS found
,
1796 reserves
.reservenotes AS reservenotes
,
1797 reserves
.priority AS priority
,
1798 reserves
.timestamp AS timestamp
,
1799 biblioitems
.biblioitemnumber AS biblioitemnumber
,
1800 reserves
.itemnumber AS itemnumber
1802 JOIN biblioitems USING
(biblionumber
)
1803 JOIN hold_fill_targets USING
(biblionumber
, borrowernumber
)
1806 AND item_level_request
= 0
1807 AND hold_fill_targets
.itemnumber
= ?
1808 AND reservedate
<= DATE_ADD
(NOW
(),INTERVAL ? DAY
)
1811 $sth = $dbh->prepare($title_level_target_query);
1812 $sth->execute($itemnumber, $lookahead||0);
1814 if ( my $data = $sth->fetchrow_hashref ) {
1815 push( @results, $data );
1817 return @results if @results;
1820 SELECT reserves
.biblionumber AS biblionumber
,
1821 reserves
.borrowernumber AS borrowernumber
,
1822 reserves
.reservedate AS reservedate
,
1823 reserves
.waitingdate AS waitingdate
,
1824 reserves
.branchcode AS branchcode
,
1825 reserves
.cancellationdate AS cancellationdate
,
1826 reserves
.found AS found
,
1827 reserves
.reservenotes AS reservenotes
,
1828 reserves
.priority AS priority
,
1829 reserves
.timestamp AS timestamp
,
1830 reserveconstraints
.biblioitemnumber AS biblioitemnumber
,
1831 reserves
.itemnumber AS itemnumber
1833 LEFT JOIN reserveconstraints ON reserves
.biblionumber
= reserveconstraints
.biblionumber
1834 WHERE reserves
.biblionumber
= ?
1835 AND
( ( reserveconstraints
.biblioitemnumber
= ?
1836 AND reserves
.borrowernumber
= reserveconstraints
.borrowernumber
1837 AND reserves
.reservedate
= reserveconstraints
.reservedate
)
1838 OR reserves
.constrainttype
='a' )
1839 AND
(reserves
.itemnumber IS NULL OR reserves
.itemnumber
= ?
)
1840 AND reserves
.reservedate
<= DATE_ADD
(NOW
(),INTERVAL ? DAY
)
1843 $sth = $dbh->prepare($query);
1844 $sth->execute( $biblio, $bibitem, $itemnumber, $lookahead||0);
1846 while ( my $data = $sth->fetchrow_hashref ) {
1847 push( @results, $data );
1852 =head2 _koha_notify_reserve
1854 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1856 Sends a notification to the patron that their hold has been filled (through
1857 ModReserveAffect, _not_ ModReserveFill)
1861 sub _koha_notify_reserve
{
1862 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1864 my $dbh = C4
::Context
->dbh;
1865 my $borrower = C4
::Members
::GetMember
(borrowernumber
=> $borrowernumber);
1867 # Try to get the borrower's email address
1868 my $to_address = C4
::Members
::GetNoticeEmailAddress
($borrowernumber);
1873 if ( $to_address || $borrower->{'smsalertnumber'} ) {
1874 $messagingprefs = C4
::Members
::Messaging
::GetMessagingPreferences
( { borrowernumber
=> $borrowernumber, message_name
=> 'Hold_Filled' } );
1879 my $sth = $dbh->prepare("
1882 WHERE borrowernumber = ?
1883 AND biblionumber = ?
1885 $sth->execute( $borrowernumber, $biblionumber );
1886 my $reserve = $sth->fetchrow_hashref;
1887 my $branch_details = GetBranchDetail
( $reserve->{'branchcode'} );
1889 my $admin_email_address = $branch_details->{'branchemail'} || C4
::Context
->preference('KohaAdminEmailAddress');
1891 my %letter_params = (
1892 module
=> 'reserves',
1893 branchcode
=> $reserve->{branchcode
},
1895 'branches' => $branch_details,
1896 'borrowers' => $borrower,
1897 'biblio' => $biblionumber,
1898 'reserves' => $reserve,
1899 'items', $reserve->{'itemnumber'},
1901 substitute
=> { today
=> C4
::Dates
->new()->output() },
1905 if ( $print_mode ) {
1906 $letter_params{ 'letter_code' } = 'HOLD_PRINT';
1907 my $letter = C4
::Letters
::GetPreparedLetter
( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1909 C4
::Letters
::EnqueueLetter
( {
1911 borrowernumber
=> $borrowernumber,
1912 message_transport_type
=> 'print',
1918 if ( $to_address && defined $messagingprefs->{transports
}->{'email'} ) {
1919 $letter_params{ 'letter_code' } = $messagingprefs->{transports
}->{'email'};
1920 my $letter = C4
::Letters
::GetPreparedLetter
( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1922 C4
::Letters
::EnqueueLetter
(
1923 { letter
=> $letter,
1924 borrowernumber
=> $borrowernumber,
1925 message_transport_type
=> 'email',
1926 from_address
=> $admin_email_address,
1931 if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports
}->{'sms'} ) {
1932 $letter_params{ 'letter_code' } = $messagingprefs->{transports
}->{'sms'};
1933 my $letter = C4
::Letters
::GetPreparedLetter
( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1935 C4
::Letters
::EnqueueLetter
(
1936 { letter
=> $letter,
1937 borrowernumber
=> $borrowernumber,
1938 message_transport_type
=> 'sms',
1944 =head2 _ShiftPriorityByDateAndPriority
1946 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1948 This increments the priority of all reserves after the one
1949 with either the lowest date after C<$reservedate>
1950 or the lowest priority after C<$priority>.
1952 It effectively makes room for a new reserve to be inserted with a certain
1953 priority, which is returned.
1955 This is most useful when the reservedate can be set by the user. It allows
1956 the new reserve to be placed before other reserves that have a later
1957 reservedate. Since priority also is set by the form in reserves/request.pl
1958 the sub accounts for that too.
1962 sub _ShiftPriorityByDateAndPriority
{
1963 my ( $biblio, $resdate, $new_priority ) = @_;
1965 my $dbh = C4
::Context
->dbh;
1966 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1967 my $sth = $dbh->prepare( $query );
1968 $sth->execute( $biblio, $resdate, $new_priority );
1969 my $min_priority = $sth->fetchrow;
1970 # if no such matches are found, $new_priority remains as original value
1971 $new_priority = $min_priority if ( $min_priority );
1973 # Shift the priority up by one; works in conjunction with the next SQL statement
1974 $query = "UPDATE reserves
1975 SET priority = priority+1
1976 WHERE biblionumber = ?
1977 AND borrowernumber = ?
1980 my $sth_update = $dbh->prepare( $query );
1982 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1983 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1984 $sth = $dbh->prepare( $query );
1985 $sth->execute( $new_priority, $biblio );
1986 while ( my $row = $sth->fetchrow_hashref ) {
1987 $sth_update->execute( $biblio, $row->{borrowernumber
}, $row->{reservedate
} );
1990 return $new_priority; # so the caller knows what priority they wind up receiving
1995 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1997 Use when checking out an item to handle reserves
1998 If $cancelreserve boolean is set to true, it will remove existing reserve
2003 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2005 my ( $restype, $res, $all_reserves ) = CheckReserves
( $itemnumber );
2008 my $biblionumber = $res->{biblionumber
};
2009 my $biblioitemnumber = $res->{biblioitemnumber
};
2011 if ($res->{borrowernumber
} == $borrowernumber) {
2012 ModReserveFill
($res);
2016 # The item is reserved by someone else.
2017 # Find this item in the reserves
2020 foreach (@
$all_reserves) {
2021 $_->{'borrowernumber'} == $borrowernumber or next;
2022 $_->{'biblionumber'} == $biblionumber or next;
2029 # The item is reserved by the current patron
2030 ModReserveFill
($borr_res);
2033 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2034 RevertWaitingStatus
({ itemnumber
=> $itemnumber });
2036 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2038 biblionumber
=> $res->{'biblionumber'},
2039 itemnumber
=> $res->{'itemnumber'},
2040 borrowernumber
=> $res->{'borrowernumber'}
2048 MergeHolds($dbh,$to_biblio, $from_biblio);
2050 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2055 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2056 my $sth = $dbh->prepare(
2057 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2059 $sth->execute($from_biblio);
2060 if ( my $data = $sth->fetchrow_hashref() ) {
2062 # holds exist on old record, if not we don't need to do anything
2063 $sth = $dbh->prepare(
2064 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2065 $sth->execute( $to_biblio, $from_biblio );
2068 # don't reorder those already waiting
2070 $sth = $dbh->prepare(
2071 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2073 my $upd_sth = $dbh->prepare(
2074 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2075 AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2077 $sth->execute( $to_biblio, 'W', 'T' );
2079 while ( my $reserve = $sth->fetchrow_hashref() ) {
2081 $priority, $to_biblio,
2082 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2083 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2090 =head2 RevertWaitingStatus
2092 $success = RevertWaitingStatus({ itemnumber => $itemnumber });
2094 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2096 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2097 item level hold, even if it was only a bibliolevel hold to
2098 begin with. This is because we can no longer know if a hold
2099 was item-level or bib-level after a hold has been set to
2104 sub RevertWaitingStatus
{
2105 my ( $params ) = @_;
2106 my $itemnumber = $params->{'itemnumber'};
2108 return unless ( $itemnumber );
2110 my $dbh = C4
::Context
->dbh;
2112 ## Get the waiting reserve we want to revert
2114 SELECT * FROM reserves
2115 WHERE itemnumber = ?
2116 AND found IS NOT NULL
2118 my $sth = $dbh->prepare( $query );
2119 $sth->execute( $itemnumber );
2120 my $reserve = $sth->fetchrow_hashref();
2122 ## Increment the priority of all other non-waiting
2123 ## reserves for this bib record
2127 priority = priority + 1
2133 $sth = $dbh->prepare( $query );
2134 $sth->execute( $reserve->{'biblionumber'} );
2136 ## Fix up the currently waiting reserve
2146 $sth = $dbh->prepare( $query );
2147 return $sth->execute( $reserve->{'reserve_id'} );
2152 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2154 Returnes the first reserve id that matches the given criteria
2159 my ( $params ) = @_;
2161 return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2163 my $dbh = C4
::Context
->dbh();
2165 my $sql = "SELECT reserve_id FROM reserves WHERE ";
2169 foreach my $key ( keys %$params ) {
2170 if ( defined( $params->{$key} ) ) {
2171 push( @limits, "$key = ?" );
2172 push( @params, $params->{$key} );
2176 $sql .= join( " AND ", @limits );
2178 my $sth = $dbh->prepare( $sql );
2179 $sth->execute( @params );
2180 my $row = $sth->fetchrow_hashref();
2182 return $row->{'reserve_id'};
2187 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2189 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2194 my ($branch, $borrowernumber, $biblionumber) = @_;
2196 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2198 my $reserve_id = GetReserveId
({
2199 biblionumber
=> $biblionumber,
2200 borrowernumber
=> $borrowernumber
2202 my $reserve = GetReserveInfo
($reserve_id) or return;
2204 return C4
::Letters
::GetPreparedLetter
(
2205 module
=> 'circulation',
2206 letter_code
=> 'RESERVESLIP',
2207 branchcode
=> $branch,
2209 'reserves' => $reserve,
2210 'branches' => $reserve->{branchcode
},
2211 'borrowers' => $reserve->{borrowernumber
},
2212 'biblio' => $reserve->{biblionumber
},
2213 'items' => $reserve->{itemnumber
},
2218 =head2 GetReservesControlBranch
2220 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2222 Return the branchcode to be used to determine which reserves
2223 policy applies to a transaction.
2225 C<$item> is a hashref for an item. Only 'homebranch' is used.
2227 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2231 sub GetReservesControlBranch
{
2232 my ( $item, $borrower ) = @_;
2234 my $reserves_control = C4
::Context
->preference('ReservesControlBranch');
2237 ( $reserves_control eq 'ItemHomeLibrary' ) ?
$item->{'homebranch'}
2238 : ( $reserves_control eq 'PatronLibrary' ) ?
$borrower->{'branchcode'}
2246 Koha Development Team <http://koha-community.org/>