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
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
25 #use warnings; FIXME - Bug 2505
33 # for _koha_notify_reserve
34 use C4::Members::Messaging;
48 use List::MoreUtils qw( firstidx any );
52 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
56 C4::Reserves - Koha functions for dealing with reservation.
64 This modules provides somes functions to deal with reservations.
66 Reserves are stored in reserves table.
67 The following columns contains important values :
68 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
69 =0 : then the reserve is being dealed
70 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
71 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
72 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
73 F(inished) : the reserve has been completed, and is done
74 - itemnumber : empty : the reserve is still unaffected to an item
75 filled: the reserve is attached to an item
76 The complete workflow is :
77 ==== 1st use case ====
78 patron request a document, 1st available : P >0, F=NULL, I=NULL
79 a library having it run "transfertodo", and clic on the list
80 if there is no transfer to do, the reserve waiting
81 patron can pick it up P =0, F=W, I=filled
82 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
83 The pickup library receive the book, it check in P =0, F=W, I=filled
84 The patron borrow the book P =0, F=F, I=filled
86 ==== 2nd use case ====
87 patron requests a document, a given item,
88 If pickup is holding branch P =0, F=W, I=filled
89 If transfer needed, write in branchtransfer P =0, F=T, I=filled
90 The pickup library receive the book, it checks it in P =0, F=W, I=filled
91 The patron borrow the book P =0, F=F, I=filled
104 &GetReservesFromItemnumber
105 &GetReservesFromBiblionumber
106 &GetReservesFromBorrowernumber
107 &GetReservesForBranch
120 &ModReserveMinusPriority
126 &CanReserveBeCanceledFromOpac
128 &CancelExpiredReserves
130 &AutoUnsuspendReserves
132 &IsAvailableForItemLevelRequest
134 &OPACItemHoldsAllowed
137 &ToggleLowestPriority
143 &GetReservesControlBranch
147 @EXPORT_OK = qw( MergeHolds );
152 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
154 Adds reserve and generates HOLDPLACED message.
156 The following tables are available witin the HOLDPLACED message:
168 $branch, $borrowernumber, $biblionumber, $bibitems,
169 $priority, $resdate, $expdate, $notes,
170 $title, $checkitem, $found, $itemtype
173 if ( Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->count() > 0 ) {
174 carp("AddReserve: borrower $borrowernumber already has a hold for biblionumber $biblionumber");
178 my $dbh = C4::Context->dbh;
180 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
181 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
183 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
185 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
187 # Make room in reserves for this before those of a later reserve date
188 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
193 # If the reserv had the waiting status, we had the value of the resdate
194 if ( $found eq 'W' ) {
195 $waitingdate = $resdate;
198 # Don't add itemtype limit if specific item is selected
199 $itemtype = undef if $checkitem;
201 # updates take place here
202 my $hold = Koha::Hold->new(
204 borrowernumber => $borrowernumber,
205 biblionumber => $biblionumber,
206 reservedate => $resdate,
207 branchcode => $branch,
208 priority => $priority,
209 reservenotes => $notes,
210 itemnumber => $checkitem,
212 waitingdate => $waitingdate,
213 expirationdate => $expdate,
214 itemtype => $itemtype,
218 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
219 if C4::Context->preference('HoldsLog');
221 my $reserve_id = $hold->id();
223 # add a reserve fee if needed
224 my $fee = GetReserveFee( $borrowernumber, $biblionumber );
225 ChargeReserveFee( $borrowernumber, $fee, $title );
227 _FixPriority({ biblionumber => $biblionumber});
229 # Send e-mail to librarian if syspref is active
230 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
231 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
232 my $library = Koha::Libraries->find($borrower->{branchcode})->unblessed;
233 if ( my $letter = C4::Letters::GetPreparedLetter (
234 module => 'reserves',
235 letter_code => 'HOLDPLACED',
236 branchcode => $branch,
238 'branches' => $library,
239 'borrowers' => $borrower,
240 'biblio' => $biblionumber,
241 'biblioitems' => $biblionumber,
242 'items' => $checkitem,
246 my $admin_email_address = $library->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
248 C4::Letters::EnqueueLetter(
250 borrowernumber => $borrowernumber,
251 message_transport_type => 'email',
252 from_address => $admin_email_address,
253 to_address => $admin_email_address,
264 $res = GetReserve( $reserve_id );
266 Return the current reserve.
271 my ($reserve_id) = @_;
273 my $dbh = C4::Context->dbh;
275 my $query = "SELECT * FROM reserves WHERE reserve_id = ?";
276 my $sth = $dbh->prepare( $query );
277 $sth->execute( $reserve_id );
278 return $sth->fetchrow_hashref();
281 =head2 GetReservesFromBiblionumber
283 my $reserves = GetReservesFromBiblionumber({
284 biblionumber => $biblionumber,
285 [ itemnumber => $itemnumber, ]
289 This function gets the list of reservations for one C<$biblionumber>,
290 returning an arrayref pointing to the reserves for C<$biblionumber>.
292 By default, only reserves whose start date falls before the current
293 time are returned. To return all reserves, including future ones,
294 the C<all_dates> parameter can be included and set to a true value.
296 If the C<itemnumber> parameter is supplied, reserves must be targeted
297 to that item or not targeted to any item at all; otherwise, they
298 are excluded from the list.
302 sub GetReservesFromBiblionumber {
304 my $biblionumber = $params->{biblionumber} or return [];
305 my $itemnumber = $params->{itemnumber};
306 my $all_dates = $params->{all_dates} // 0;
307 my $dbh = C4::Context->dbh;
309 # Find the desired items in the reserves
314 timestamp AS rtimestamp,
328 WHERE biblionumber = ? ";
329 push( @params, $biblionumber );
330 unless ( $all_dates ) {
331 $query .= " AND reservedate <= CAST(NOW() AS DATE) ";
334 $query .= " AND ( itemnumber IS NULL OR itemnumber = ? )";
335 push( @params, $itemnumber );
337 $query .= "ORDER BY priority";
338 my $sth = $dbh->prepare($query);
339 $sth->execute( @params );
341 while ( my $data = $sth->fetchrow_hashref ) {
342 push @results, $data;
347 =head2 GetReservesFromItemnumber
349 ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $waitingdate ) = GetReservesFromItemnumber($itemnumber);
351 Get the first reserve for a specific item number (based on priority). Returns the abovementioned values for that reserve.
353 The routine does not look at future reserves (read: item level holds), but DOES include future waits (a confirmed future hold).
357 sub GetReservesFromItemnumber {
358 my ($itemnumber) = @_;
360 my $schema = Koha::Database->new()->schema();
362 my $r = $schema->resultset('Reserve')->search(
364 itemnumber => $itemnumber,
367 reservedate => \'<= CAST( NOW() AS DATE )',
368 waitingdate => { '!=', undef }
372 order_by => 'priority',
380 $r->get_column('borrowernumber'),
381 $r->get_column('branchcode'),
387 =head2 GetReservesFromBorrowernumber
389 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
395 sub GetReservesFromBorrowernumber {
396 my ( $borrowernumber, $status ) = @_;
397 my $dbh = C4::Context->dbh;
400 $sth = $dbh->prepare("
403 WHERE borrowernumber=?
407 $sth->execute($borrowernumber,$status);
409 $sth = $dbh->prepare("
412 WHERE borrowernumber=?
415 $sth->execute($borrowernumber);
417 my $data = $sth->fetchall_arrayref({});
421 =head2 CanBookBeReserved
423 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber)
424 if ($canReserve eq 'OK') { #We can reserve this Item! }
426 See CanItemBeReserved() for possible return values.
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);
441 foreach my $item (@$items) {
442 $canReserve = CanItemBeReserved( $borrowernumber, $item );
443 return 'OK' if $canReserve eq 'OK';
448 =head2 CanItemBeReserved
450 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber)
451 if ($canReserve eq 'OK') { #We can reserve this Item! }
453 @RETURNS OK, if the Item can be reserved.
454 ageRestricted, if the Item is age restricted for this borrower.
455 damaged, if the Item is damaged.
456 cannotReserveFromOtherBranches, if syspref 'canreservefromotherbranches' is OK.
457 tooManyReserves, if the borrower has exceeded his maximum reserve amount.
458 notReservable, if holds on this item are not allowed
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);
472 my $biblioData = C4::Biblio::GetBiblioData( $item->{biblionumber} );
473 my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
475 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
476 return 'damaged' if ( $item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems') );
478 #Check for the age restriction
479 my ($ageRestriction, $daysToAgeRestriction) = C4::Circulation::GetAgeRestriction( $biblioData->{agerestriction}, $borrower );
480 return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0;
482 my $controlbranch = C4::Context->preference('ReservesControlBranch');
484 # we retrieve user rights on this itemtype and branchcode
485 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
487 WHERE (categorycode in (?,'*') )
488 AND (itemtype IN (?,'*'))
489 AND (branchcode IN (?,'*'))
496 my $querycount ="SELECT
499 LEFT JOIN items USING (itemnumber)
500 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
501 LEFT JOIN borrowers USING (borrowernumber)
502 WHERE borrowernumber = ?
507 my $branchfield = "reserves.branchcode";
509 if( $controlbranch eq "ItemHomeLibrary" ){
510 $branchfield = "items.homebranch";
511 $branchcode = $item->{homebranch};
512 }elsif( $controlbranch eq "PatronLibrary" ){
513 $branchfield = "borrowers.branchcode";
514 $branchcode = $borrower->{branchcode};
518 $sth->execute($borrower->{'categorycode'}, $item->{'itype'}, $branchcode);
519 if(my $rights = $sth->fetchrow_hashref()){
520 $ruleitemtype = $rights->{itemtype};
521 $allowedreserves = $rights->{reservesallowed};
528 $querycount .= "AND $branchfield = ?";
530 # If using item-level itypes, fall back to the record
531 # level itemtype if the hold has no associated item
533 C4::Context->preference('item-level_itypes')
534 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
535 : " AND biblioitems.itemtype = ?"
536 if ( $ruleitemtype ne "*" );
538 my $sthcount = $dbh->prepare($querycount);
540 if($ruleitemtype eq "*"){
541 $sthcount->execute($borrowernumber, $branchcode);
543 $sthcount->execute($borrowernumber, $branchcode, $ruleitemtype);
546 my $reservecount = "0";
547 if(my $rowcount = $sthcount->fetchrow_hashref()){
548 $reservecount = $rowcount->{count};
550 # we check if it's ok or not
551 if( $reservecount >= $allowedreserves ){
552 return 'tooManyReserves';
555 my $circ_control_branch = C4::Circulation::_GetCircControlBranch($item,
557 my $branchitemrule = C4::Circulation::GetBranchItemRule($circ_control_branch,
560 if ( $branchitemrule->{holdallowed} == 0 ) {
561 return 'notReservable';
564 if ( $branchitemrule->{holdallowed} == 1
565 && $borrower->{branchcode} ne $item->{homebranch} )
567 return 'cannotReserveFromOtherBranches';
570 # If reservecount is ok, we check item branch if IndependentBranches is ON
571 # and canreservefromotherbranches is OFF
572 if ( C4::Context->preference('IndependentBranches')
573 and !C4::Context->preference('canreservefromotherbranches') )
575 my $itembranch = $item->{homebranch};
576 if ($itembranch ne $borrower->{branchcode}) {
577 return 'cannotReserveFromOtherBranches';
584 =head2 CanReserveBeCanceledFromOpac
586 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
588 returns 1 if reserve can be cancelled by user from OPAC.
589 First check if reserve belongs to user, next checks if reserve is not in
590 transfer or waiting status
594 sub CanReserveBeCanceledFromOpac {
595 my ($reserve_id, $borrowernumber) = @_;
597 return unless $reserve_id and $borrowernumber;
598 my $reserve = GetReserve($reserve_id);
600 return 0 unless $reserve->{borrowernumber} == $borrowernumber;
601 return 0 if ( $reserve->{found} eq 'W' ) or ( $reserve->{found} eq 'T' );
607 =head2 GetReserveCount
609 $number = &GetReserveCount($borrowernumber);
611 this function returns the number of reservation for a borrower given on input arg.
615 sub GetReserveCount {
616 my ($borrowernumber) = @_;
618 my $dbh = C4::Context->dbh;
621 SELECT COUNT(*) AS counter
623 WHERE borrowernumber = ?
625 my $sth = $dbh->prepare($query);
626 $sth->execute($borrowernumber);
627 my $row = $sth->fetchrow_hashref;
628 return $row->{counter};
631 =head2 GetOtherReserves
633 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
635 Check queued list of this document and check if this document must be transferred
639 sub GetOtherReserves {
640 my ($itemnumber) = @_;
643 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
644 if ($checkreserves) {
645 my $iteminfo = GetItem($itemnumber);
646 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
647 $messages->{'transfert'} = $checkreserves->{'branchcode'};
648 #minus priorities of others reservs
649 ModReserveMinusPriority(
651 $checkreserves->{'reserve_id'},
654 #launch the subroutine dotransfer
655 C4::Items::ModItemTransfer(
657 $iteminfo->{'holdingbranch'},
658 $checkreserves->{'branchcode'}
663 #step 2b : case of a reservation on the same branch, set the waiting status
665 $messages->{'waiting'} = 1;
666 ModReserveMinusPriority(
668 $checkreserves->{'reserve_id'},
670 ModReserveStatus($itemnumber,'W');
673 $nextreservinfo = $checkreserves->{'borrowernumber'};
676 return ( $messages, $nextreservinfo );
679 =head2 ChargeReserveFee
681 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
683 Charge the fee for a reserve (if $fee > 0)
687 sub ChargeReserveFee {
688 my ( $borrowernumber, $fee, $title ) = @_;
689 return if !$fee || $fee==0; # the last test is needed to include 0.00
691 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
693 my $dbh = C4::Context->dbh;
694 my $nextacctno = &getnextacctno( $borrowernumber );
695 $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
700 $fee = GetReserveFee( $borrowernumber, $biblionumber );
702 Calculate the fee for a reserve (if applicable).
707 my ( $borrowernumber, $biblionumber ) = @_;
709 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
712 SELECT COUNT(*) FROM items
713 LEFT JOIN issues USING (itemnumber)
714 WHERE items.biblionumber=? AND issues.issue_id IS NULL
717 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
720 my $dbh = C4::Context->dbh;
721 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
722 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
723 if( $fee and $fee > 0 and $hold_fee_mode ne 'always' ) {
724 # This is a reconstruction of the old code:
725 # Compare number of items with items issued, and optionally check holds
726 # If not all items are issued and there are no holds: charge no fee
727 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
728 my ( $notissued, $reserved );
729 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
732 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
733 ( $biblionumber, $borrowernumber ) );
734 $fee = 0 if $reserved == 0;
740 =head2 GetReservesToBranch
742 @transreserv = GetReservesToBranch( $frombranch );
744 Get reserve list for a given branch
748 sub GetReservesToBranch {
749 my ( $frombranch ) = @_;
750 my $dbh = C4::Context->dbh;
751 my $sth = $dbh->prepare(
752 "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
757 $sth->execute( $frombranch );
760 while ( my $data = $sth->fetchrow_hashref ) {
761 $transreserv[$i] = $data;
764 return (@transreserv);
767 =head2 GetReservesForBranch
769 @transreserv = GetReservesForBranch($frombranch);
773 sub GetReservesForBranch {
774 my ($frombranch) = @_;
775 my $dbh = C4::Context->dbh;
778 SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
783 $query .= " AND branchcode=? " if ( $frombranch );
784 $query .= "ORDER BY waitingdate" ;
786 my $sth = $dbh->prepare($query);
788 $sth->execute($frombranch);
795 while ( my $data = $sth->fetchrow_hashref ) {
796 $transreserv[$i] = $data;
799 return (@transreserv);
802 =head2 GetReserveStatus
804 $reservestatus = GetReserveStatus($itemnumber);
806 Takes an itemnumber and returns the status of the reserve placed on it.
807 If several reserves exist, the reserve with the lower priority is given.
811 ## FIXME: I don't think this does what it thinks it does.
812 ## It only ever checks the first reserve result, even though
813 ## multiple reserves for that bib can have the itemnumber set
814 ## the sub is only used once in the codebase.
815 sub GetReserveStatus {
816 my ($itemnumber) = @_;
818 my $dbh = C4::Context->dbh;
820 my ($sth, $found, $priority);
822 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
823 $sth->execute($itemnumber);
824 ($found, $priority) = $sth->fetchrow_array;
828 return 'Waiting' if $found eq 'W' and $priority == 0;
829 return 'Finished' if $found eq 'F';
832 return 'Reserved' if $priority > 0;
834 return ''; # empty string here will remove need for checking undef, or less log lines
839 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
840 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
841 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
843 Find a book in the reserves.
845 C<$itemnumber> is the book's item number.
846 C<$lookahead> is the number of days to look in advance for future reserves.
848 As I understand it, C<&CheckReserves> looks for the given item in the
849 reserves. If it is found, that's a match, and C<$status> is set to
852 Otherwise, it finds the most important item in the reserves with the
853 same biblio number as this book (I'm not clear on this) and returns it
854 with C<$status> set to C<Reserved>.
856 C<&CheckReserves> returns a two-element list:
858 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
860 C<$reserve> is the reserve item that matched. It is a
861 reference-to-hash whose keys are mostly the fields of the reserves
862 table in the Koha database.
867 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
868 my $dbh = C4::Context->dbh;
871 if (C4::Context->preference('item-level_itypes')){
873 SELECT items.biblionumber,
874 items.biblioitemnumber,
875 itemtypes.notforloan,
876 items.notforloan AS itemnotforloan,
882 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
883 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
888 SELECT items.biblionumber,
889 items.biblioitemnumber,
890 itemtypes.notforloan,
891 items.notforloan AS itemnotforloan,
897 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
898 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
903 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
904 $sth->execute($item);
907 $sth = $dbh->prepare("$select WHERE barcode = ?");
908 $sth->execute($barcode);
910 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
911 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
913 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
915 return unless $itemnumber; # bail if we got nothing.
917 # if item is not for loan it cannot be reserved either.....
918 # except where items.notforloan < 0 : This indicates the item is holdable.
919 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
921 # Find this item in the reserves
922 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
924 # $priority and $highest are used to find the most important item
925 # in the list returned by &_Findgroupreserve. (The lower $priority,
926 # the more important the item.)
927 # $highest is the most important item we've seen so far.
929 if (scalar @reserves) {
930 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
931 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
932 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
934 my $priority = 10000000;
935 foreach my $res (@reserves) {
936 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
937 return ( "Waiting", $res, \@reserves ); # Found it
941 my $local_hold_match;
943 if ($LocalHoldsPriority) {
944 $borrowerinfo = C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
945 $iteminfo = C4::Items::GetItem($itemnumber);
947 my $local_holds_priority_item_branchcode =
948 $iteminfo->{$LocalHoldsPriorityItemControl};
949 my $local_holds_priority_patron_branchcode =
950 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
952 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
953 ? $borrowerinfo->{branchcode}
956 $local_holds_priority_item_branchcode eq
957 $local_holds_priority_patron_branchcode;
960 # See if this item is more important than what we've got so far
961 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
962 $iteminfo ||= C4::Items::GetItem($itemnumber);
963 next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
964 $borrowerinfo ||= C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
965 my $branch = GetReservesControlBranch( $iteminfo, $borrowerinfo );
966 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
967 next if ($branchitemrule->{'holdallowed'} == 0);
968 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
969 next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
970 $priority = $res->{'priority'};
972 last if $local_hold_match;
978 # If we get this far, then no exact match was found.
979 # We return the most important (i.e. next) reservation.
981 $highest->{'itemnumber'} = $item;
982 return ( "Reserved", $highest, \@reserves );
988 =head2 CancelExpiredReserves
990 CancelExpiredReserves();
992 Cancels all reserves with an expiration date from before today.
996 sub CancelExpiredReserves {
998 # Cancel reserves that have passed their expiration date.
999 my $dbh = C4::Context->dbh;
1000 my $sth = $dbh->prepare( "
1001 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
1002 AND expirationdate IS NOT NULL
1007 while ( my $res = $sth->fetchrow_hashref() ) {
1008 CancelReserve({ reserve_id => $res->{'reserve_id'} });
1011 # Cancel reserves that have been waiting too long
1012 if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
1013 my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
1014 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
1016 my $today = dt_from_string();
1018 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
1019 $sth = $dbh->prepare( $query );
1020 $sth->execute( $max_pickup_delay );
1022 while ( my $res = $sth->fetchrow_hashref ) {
1024 unless ( $cancel_on_holidays ) {
1025 my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
1026 my $is_holiday = $calendar->is_holiday( $today );
1028 if ( $is_holiday ) {
1034 CancelReserve({ reserve_id => $res->{'reserve_id'}, charge_cancel_fee => 1 });
1041 =head2 AutoUnsuspendReserves
1043 AutoUnsuspendReserves();
1045 Unsuspends all suspended reserves with a suspend_until date from before today.
1049 sub AutoUnsuspendReserves {
1050 my $today = dt_from_string();
1052 my @holds = Koha::Holds->search( { suspend_until => { '<' => $today->ymd() } } );
1054 map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
1057 =head2 CancelReserve
1059 CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber, ] [ charge_cancel_fee => 1 ] });
1061 Cancels a reserve. If C<charge_cancel_fee> is passed and the C<ExpireReservesMaxPickUpDelayCharge> syspref is set, charge that fee to the patron's account.
1066 my ( $params ) = @_;
1068 my $reserve_id = $params->{'reserve_id'};
1069 # Filter out only the desired keys; this will insert undefined values for elements missing in
1070 # \%params, but GetReserveId filters them out anyway.
1071 $reserve_id = GetReserveId( { biblionumber => $params->{'biblionumber'}, borrowernumber => $params->{'borrowernumber'}, itemnumber => $params->{'itemnumber'} } ) unless ( $reserve_id );
1073 return unless ( $reserve_id );
1075 my $dbh = C4::Context->dbh;
1077 my $reserve = GetReserve( $reserve_id );
1080 my $hold = Koha::Holds->find( $reserve_id );
1081 logaction( 'HOLDS', 'CANCEL', $hold->reserve_id, Dumper($hold->unblessed) )
1082 if C4::Context->preference('HoldsLog');
1086 SET cancellationdate = now(),
1088 WHERE reserve_id = ?
1090 my $sth = $dbh->prepare($query);
1091 $sth->execute( $reserve_id );
1094 INSERT INTO old_reserves
1095 SELECT * FROM reserves
1096 WHERE reserve_id = ?
1098 $sth = $dbh->prepare($query);
1099 $sth->execute( $reserve_id );
1102 DELETE FROM reserves
1103 WHERE reserve_id = ?
1105 $sth = $dbh->prepare($query);
1106 $sth->execute( $reserve_id );
1108 # now fix the priority on the others....
1109 _FixPriority({ biblionumber => $reserve->{biblionumber} });
1111 # and, if desired, charge a cancel fee
1112 my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
1113 if ( $charge && $params->{'charge_cancel_fee'} ) {
1114 manualinvoice($reserve->{'borrowernumber'}, $reserve->{'itemnumber'}, '', 'HE', $charge);
1123 ModReserve({ rank => $rank,
1124 reserve_id => $reserve_id,
1125 branchcode => $branchcode
1126 [, itemnumber => $itemnumber ]
1127 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1130 Change a hold request's priority or cancel it.
1132 C<$rank> specifies the effect of the change. If C<$rank>
1133 is 'W' or 'n', nothing happens. This corresponds to leaving a
1134 request alone when changing its priority in the holds queue
1137 If C<$rank> is 'del', the hold request is cancelled.
1139 If C<$rank> is an integer greater than zero, the priority of
1140 the request is set to that value. Since priority != 0 means
1141 that the item is not waiting on the hold shelf, setting the
1142 priority to a non-zero value also sets the request's found
1143 status and waiting date to NULL.
1145 The optional C<$itemnumber> parameter is used only when
1146 C<$rank> is a non-zero integer; if supplied, the itemnumber
1147 of the hold request is set accordingly; if omitted, the itemnumber
1150 B<FIXME:> Note that the forgoing can have the effect of causing
1151 item-level hold requests to turn into title-level requests. This
1152 will be fixed once reserves has separate columns for requested
1153 itemnumber and supplying itemnumber.
1158 my ( $params ) = @_;
1160 my $rank = $params->{'rank'};
1161 my $reserve_id = $params->{'reserve_id'};
1162 my $branchcode = $params->{'branchcode'};
1163 my $itemnumber = $params->{'itemnumber'};
1164 my $suspend_until = $params->{'suspend_until'};
1165 my $borrowernumber = $params->{'borrowernumber'};
1166 my $biblionumber = $params->{'biblionumber'};
1168 return if $rank eq "W";
1169 return if $rank eq "n";
1171 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1172 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1174 my $dbh = C4::Context->dbh;
1175 if ( $rank eq "del" ) {
1176 CancelReserve({ reserve_id => $reserve_id });
1178 elsif ($rank =~ /^\d+/ and $rank > 0) {
1179 my $hold = Koha::Holds->find($reserve_id);
1180 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1181 if C4::Context->preference('HoldsLog');
1186 branchcode => $branchcode,
1187 itemnumber => $itemnumber,
1189 waitingdate => undef
1193 if ( defined( $suspend_until ) ) {
1194 if ( $suspend_until ) {
1195 $suspend_until = eval { dt_from_string( $suspend_until ) };
1196 $hold->suspend_hold( $suspend_until );
1198 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1199 # If the hold is not suspended, this does nothing.
1200 $hold->set( { suspend_until => undef } )->store();
1204 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1208 =head2 ModReserveFill
1210 &ModReserveFill($reserve);
1212 Fill a reserve. If I understand this correctly, this means that the
1213 reserved book has been found and given to the patron who reserved it.
1215 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1216 whose keys are fields from the reserves table in the Koha database.
1220 sub ModReserveFill {
1222 my $dbh = C4::Context->dbh;
1223 # fill in a reserve record....
1224 my $reserve_id = $res->{'reserve_id'};
1225 my $biblionumber = $res->{'biblionumber'};
1226 my $borrowernumber = $res->{'borrowernumber'};
1227 my $resdate = $res->{'reservedate'};
1229 # get the priority on this record....
1231 my $query = "SELECT priority
1233 WHERE biblionumber = ?
1234 AND borrowernumber = ?
1235 AND reservedate = ?";
1236 my $sth = $dbh->prepare($query);
1237 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1238 ($priority) = $sth->fetchrow_array;
1240 # update the database...
1241 $query = "UPDATE reserves
1244 WHERE biblionumber = ?
1246 AND borrowernumber = ?
1248 $sth = $dbh->prepare($query);
1249 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1251 # move to old_reserves
1252 $query = "INSERT INTO old_reserves
1253 SELECT * FROM reserves
1254 WHERE biblionumber = ?
1256 AND borrowernumber = ?
1258 $sth = $dbh->prepare($query);
1259 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1260 $query = "DELETE FROM reserves
1261 WHERE biblionumber = ?
1263 AND borrowernumber = ?
1265 $sth = $dbh->prepare($query);
1266 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1268 # now fix the priority on the others (if the priority wasn't
1269 # already sorted!)....
1270 unless ( $priority == 0 ) {
1271 _FixPriority({ reserve_id => $reserve_id, biblionumber => $biblionumber });
1275 =head2 ModReserveStatus
1277 &ModReserveStatus($itemnumber, $newstatus);
1279 Update the reserve status for the active (priority=0) reserve.
1281 $itemnumber is the itemnumber the reserve is on
1283 $newstatus is the new status.
1287 sub ModReserveStatus {
1289 #first : check if we have a reservation for this item .
1290 my ($itemnumber, $newstatus) = @_;
1291 my $dbh = C4::Context->dbh;
1293 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1294 my $sth_set = $dbh->prepare($query);
1295 $sth_set->execute( $newstatus, $itemnumber );
1297 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1298 CartToShelf( $itemnumber );
1302 =head2 ModReserveAffect
1304 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1306 This function affect an item and a status for a given reserve
1307 The itemnumber parameter is used to find the biblionumber.
1308 with the biblionumber & the borrowernumber, we can affect the itemnumber
1309 to the correct reserve.
1311 if $transferToDo is not set, then the status is set to "Waiting" as well.
1312 otherwise, a transfer is on the way, and the end of the transfer will
1313 take care of the waiting status
1317 sub ModReserveAffect {
1318 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1319 my $dbh = C4::Context->dbh;
1321 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1322 # attached to $itemnumber
1323 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1324 $sth->execute($itemnumber);
1325 my ($biblionumber) = $sth->fetchrow;
1327 # get request - need to find out if item is already
1328 # waiting in order to not send duplicate hold filled notifications
1329 my $reserve_id = GetReserveId({
1330 borrowernumber => $borrowernumber,
1331 biblionumber => $biblionumber,
1333 return unless defined $reserve_id;
1334 my $request = GetReserveInfo($reserve_id);
1335 my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1337 # If we affect a reserve that has to be transferred, don't set to Waiting
1339 if ($transferToDo) {
1345 WHERE borrowernumber = ?
1346 AND biblionumber = ?
1350 # affect the reserve to Waiting as well.
1355 waitingdate = NOW(),
1357 WHERE borrowernumber = ?
1358 AND biblionumber = ?
1361 $sth = $dbh->prepare($query);
1362 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1363 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1364 _FixPriority( { biblionumber => $biblionumber } );
1365 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1366 CartToShelf( $itemnumber );
1372 =head2 ModReserveCancelAll
1374 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1376 function to cancel reserv,check other reserves, and transfer document if it's necessary
1380 sub ModReserveCancelAll {
1383 my ( $itemnumber, $borrowernumber ) = @_;
1385 #step 1 : cancel the reservation
1386 my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1388 #step 2 launch the subroutine of the others reserves
1389 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1391 return ( $messages, $nextreservinfo );
1394 =head2 ModReserveMinusPriority
1396 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1398 Reduce the values of queued list
1402 sub ModReserveMinusPriority {
1403 my ( $itemnumber, $reserve_id ) = @_;
1405 #first step update the value of the first person on reserv
1406 my $dbh = C4::Context->dbh;
1409 SET priority = 0 , itemnumber = ?
1410 WHERE reserve_id = ?
1412 my $sth_upd = $dbh->prepare($query);
1413 $sth_upd->execute( $itemnumber, $reserve_id );
1414 # second step update all others reserves
1415 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1418 =head2 GetReserveInfo
1420 &GetReserveInfo($reserve_id);
1422 Get item and borrower details for a current hold.
1423 Current implementation this query should have a single result.
1427 sub GetReserveInfo {
1428 my ( $reserve_id ) = @_;
1429 my $dbh = C4::Context->dbh;
1434 reserves.borrowernumber,
1435 reserves.biblionumber,
1436 reserves.branchcode,
1437 reserves.waitingdate,
1453 items.holdingbranch,
1454 items.itemcallnumber,
1460 LEFT JOIN items USING(itemnumber)
1461 LEFT JOIN borrowers USING(borrowernumber)
1462 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1463 WHERE reserves.reserve_id = ?";
1464 my $sth = $dbh->prepare($strsth);
1465 $sth->execute($reserve_id);
1467 my $data = $sth->fetchrow_hashref;
1471 =head2 IsAvailableForItemLevelRequest
1473 my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1475 Checks whether a given item record is available for an
1476 item-level hold request. An item is available if
1478 * it is not lost AND
1479 * it is not damaged AND
1480 * it is not withdrawn AND
1481 * does not have a not for loan value > 0
1483 Need to check the issuingrules onshelfholds column,
1484 if this is set items on the shelf can be placed on hold
1486 Note that IsAvailableForItemLevelRequest() does not
1487 check if the staff operator is authorized to place
1488 a request on the item - in particular,
1489 this routine does not check IndependentBranches
1490 and canreservefromotherbranches.
1494 sub IsAvailableForItemLevelRequest {
1496 my $borrower = shift;
1498 my $dbh = C4::Context->dbh;
1499 # must check the notforloan setting of the itemtype
1500 # FIXME - a lot of places in the code do this
1501 # or something similar - need to be
1503 my $itype = _get_itype($item);
1504 my $notforloan_per_itemtype
1505 = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1509 $notforloan_per_itemtype ||
1510 $item->{itemlost} ||
1511 $item->{notforloan} > 0 ||
1512 $item->{withdrawn} ||
1513 ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1515 my $on_shelf_holds = _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1517 if ( $on_shelf_holds == 1 ) {
1519 } elsif ( $on_shelf_holds == 2 ) {
1521 Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1523 my $any_available = 0;
1525 foreach my $i (@items) {
1528 || $i->{notforloan} > 0
1531 || IsItemOnHoldAndFound( $i->id )
1533 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1534 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan;
1537 return $any_available ? 0 : 1;
1540 return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting";
1543 =head2 OnShelfHoldsAllowed
1545 OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode);
1547 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf
1548 holds are allowed, returns true if so.
1552 sub OnShelfHoldsAllowed {
1553 my ($item, $borrower) = @_;
1555 my $itype = _get_itype($item);
1556 return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1563 if (C4::Context->preference('item-level_itypes')) {
1564 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1565 # When GetItem is fixed, we can remove this
1566 $itype = $item->{itype};
1569 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1570 # So if we already have a biblioitems join when calling this function,
1571 # we don't need to access the database again
1572 $itype = $item->{itemtype};
1575 my $dbh = C4::Context->dbh;
1576 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1577 my $sth = $dbh->prepare($query);
1578 $sth->execute($item->{biblioitemnumber});
1579 if (my $data = $sth->fetchrow_hashref()){
1580 $itype = $data->{itemtype};
1586 sub _OnShelfHoldsAllowed {
1587 my ($itype,$borrowercategory,$branchcode) = @_;
1589 my $rule = C4::Circulation::GetIssuingRule($borrowercategory, $itype, $branchcode);
1590 return $rule->{onshelfholds};
1593 =head2 AlterPriority
1595 AlterPriority( $where, $reserve_id );
1597 This function changes a reserve's priority up, down, to the top, or to the bottom.
1598 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1603 my ( $where, $reserve_id ) = @_;
1605 my $dbh = C4::Context->dbh;
1607 my $reserve = GetReserve( $reserve_id );
1609 if ( $reserve->{cancellationdate} ) {
1610 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1614 if ( $where eq 'up' || $where eq 'down' ) {
1616 my $priority = $reserve->{'priority'};
1617 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1618 _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1620 } elsif ( $where eq 'top' ) {
1622 _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1624 } elsif ( $where eq 'bottom' ) {
1626 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1631 =head2 ToggleLowestPriority
1633 ToggleLowestPriority( $borrowernumber, $biblionumber );
1635 This function sets the lowestPriority field to true if is false, and false if it is true.
1639 sub ToggleLowestPriority {
1640 my ( $reserve_id ) = @_;
1642 my $dbh = C4::Context->dbh;
1644 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1645 $sth->execute( $reserve_id );
1647 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1650 =head2 ToggleSuspend
1652 ToggleSuspend( $reserve_id );
1654 This function sets the suspend field to true if is false, and false if it is true.
1655 If the reserve is currently suspended with a suspend_until date, that date will
1656 be cleared when it is unsuspended.
1661 my ( $reserve_id, $suspend_until ) = @_;
1663 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1665 my $hold = Koha::Holds->find( $reserve_id );
1667 if ( $hold->is_suspended ) {
1670 $hold->suspend_hold( $suspend_until );
1677 borrowernumber => $borrowernumber,
1678 [ biblionumber => $biblionumber, ]
1679 [ suspend_until => $suspend_until, ]
1680 [ suspend => $suspend ]
1683 This function accepts a set of hash keys as its parameters.
1684 It requires either borrowernumber or biblionumber, or both.
1686 suspend_until is wholly optional.
1693 my $borrowernumber = $params{'borrowernumber'} || undef;
1694 my $biblionumber = $params{'biblionumber'} || undef;
1695 my $suspend_until = $params{'suspend_until'} || undef;
1696 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1698 $suspend_until = eval { dt_from_string($suspend_until) }
1699 if ( defined($suspend_until) );
1701 return unless ( $borrowernumber || $biblionumber );
1704 $params->{found} = undef;
1705 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1706 $params->{biblionumber} = $biblionumber if $biblionumber;
1708 my @holds = Koha::Holds->search($params);
1711 map { $_->suspend_hold($suspend_until) } @holds;
1714 map { $_->resume() } @holds;
1722 reserve_id => $reserve_id,
1724 [ignoreSetLowestRank => $ignoreSetLowestRank]
1729 _FixPriority({ biblionumber => $biblionumber});
1731 This routine adjusts the priority of a hold request and holds
1734 In the first form, where a reserve_id is passed, the priority of the
1735 hold is set to supplied rank, and other holds for that bib are adjusted
1736 accordingly. If the rank is "del", the hold is cancelled. If no rank
1737 is supplied, all of the holds on that bib have their priority adjusted
1738 as if the second form had been used.
1740 In the second form, where a biblionumber is passed, the holds on that
1741 bib (that are not captured) are sorted in order of increasing priority,
1742 then have reserves.priority set so that the first non-captured hold
1743 has its priority set to 1, the second non-captured hold has its priority
1744 set to 2, and so forth.
1746 In both cases, holds that have the lowestPriority flag on are have their
1747 priority adjusted to ensure that they remain at the end of the line.
1749 Note that the ignoreSetLowestRank parameter is meant to be used only
1750 when _FixPriority calls itself.
1755 my ( $params ) = @_;
1756 my $reserve_id = $params->{reserve_id};
1757 my $rank = $params->{rank} // '';
1758 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1759 my $biblionumber = $params->{biblionumber};
1761 my $dbh = C4::Context->dbh;
1763 unless ( $biblionumber ) {
1764 my $res = GetReserve( $reserve_id );
1765 $biblionumber = $res->{biblionumber};
1768 if ( $rank eq "del" ) {
1769 CancelReserve({ reserve_id => $reserve_id });
1771 elsif ( $rank eq "W" || $rank eq "0" ) {
1773 # make sure priority for waiting or in-transit items is 0
1777 WHERE reserve_id = ?
1778 AND found IN ('W', 'T')
1780 my $sth = $dbh->prepare($query);
1781 $sth->execute( $reserve_id );
1787 SELECT reserve_id, borrowernumber, reservedate
1789 WHERE biblionumber = ?
1790 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1791 ORDER BY priority ASC
1793 my $sth = $dbh->prepare($query);
1794 $sth->execute( $biblionumber );
1795 while ( my $line = $sth->fetchrow_hashref ) {
1796 push( @priority, $line );
1799 # To find the matching index
1801 my $key = -1; # to allow for 0 to be a valid result
1802 for ( $i = 0 ; $i < @priority ; $i++ ) {
1803 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1804 $key = $i; # save the index
1809 # if index exists in array then move it to new position
1810 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1811 my $new_rank = $rank -
1812 1; # $new_rank is what you want the new index to be in the array
1813 my $moving_item = splice( @priority, $key, 1 );
1814 splice( @priority, $new_rank, 0, $moving_item );
1817 # now fix the priority on those that are left....
1821 WHERE reserve_id = ?
1823 $sth = $dbh->prepare($query);
1824 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1827 $priority[$j]->{'reserve_id'}
1831 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1834 unless ( $ignoreSetLowestRank ) {
1835 while ( my $res = $sth->fetchrow_hashref() ) {
1837 reserve_id => $res->{'reserve_id'},
1839 ignoreSetLowestRank => 1
1845 =head2 _Findgroupreserve
1847 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1849 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1850 first match found. If neither, then we look for non-holds-queue based holds.
1851 Lookahead is the number of days to look in advance.
1853 C<&_Findgroupreserve> returns :
1854 C<@results> is an array of references-to-hash whose keys are mostly
1855 fields from the reserves table of the Koha database, plus
1856 C<biblioitemnumber>.
1860 sub _Findgroupreserve {
1861 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1862 my $dbh = C4::Context->dbh;
1864 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1865 # check for exact targeted match
1866 my $item_level_target_query = qq{
1867 SELECT reserves.biblionumber AS biblionumber,
1868 reserves.borrowernumber AS borrowernumber,
1869 reserves.reservedate AS reservedate,
1870 reserves.branchcode AS branchcode,
1871 reserves.cancellationdate AS cancellationdate,
1872 reserves.found AS found,
1873 reserves.reservenotes AS reservenotes,
1874 reserves.priority AS priority,
1875 reserves.timestamp AS timestamp,
1876 biblioitems.biblioitemnumber AS biblioitemnumber,
1877 reserves.itemnumber AS itemnumber,
1878 reserves.reserve_id AS reserve_id,
1879 reserves.itemtype AS itemtype
1881 JOIN biblioitems USING (biblionumber)
1882 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1885 AND item_level_request = 1
1887 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1891 my $sth = $dbh->prepare($item_level_target_query);
1892 $sth->execute($itemnumber, $lookahead||0);
1894 if ( my $data = $sth->fetchrow_hashref ) {
1895 push( @results, $data )
1896 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1898 return @results if @results;
1900 # check for title-level targeted match
1901 my $title_level_target_query = qq{
1902 SELECT reserves.biblionumber AS biblionumber,
1903 reserves.borrowernumber AS borrowernumber,
1904 reserves.reservedate AS reservedate,
1905 reserves.branchcode AS branchcode,
1906 reserves.cancellationdate AS cancellationdate,
1907 reserves.found AS found,
1908 reserves.reservenotes AS reservenotes,
1909 reserves.priority AS priority,
1910 reserves.timestamp AS timestamp,
1911 biblioitems.biblioitemnumber AS biblioitemnumber,
1912 reserves.itemnumber AS itemnumber,
1913 reserves.reserve_id AS reserve_id,
1914 reserves.itemtype AS itemtype
1916 JOIN biblioitems USING (biblionumber)
1917 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1920 AND item_level_request = 0
1921 AND hold_fill_targets.itemnumber = ?
1922 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1926 $sth = $dbh->prepare($title_level_target_query);
1927 $sth->execute($itemnumber, $lookahead||0);
1929 if ( my $data = $sth->fetchrow_hashref ) {
1930 push( @results, $data )
1931 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1933 return @results if @results;
1936 SELECT reserves.biblionumber AS biblionumber,
1937 reserves.borrowernumber AS borrowernumber,
1938 reserves.reservedate AS reservedate,
1939 reserves.waitingdate AS waitingdate,
1940 reserves.branchcode AS branchcode,
1941 reserves.cancellationdate AS cancellationdate,
1942 reserves.found AS found,
1943 reserves.reservenotes AS reservenotes,
1944 reserves.priority AS priority,
1945 reserves.timestamp AS timestamp,
1946 reserves.itemnumber AS itemnumber,
1947 reserves.reserve_id AS reserve_id,
1948 reserves.itemtype AS itemtype
1950 WHERE reserves.biblionumber = ?
1951 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1952 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1956 $sth = $dbh->prepare($query);
1957 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1959 while ( my $data = $sth->fetchrow_hashref ) {
1960 push( @results, $data )
1961 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1966 =head2 _koha_notify_reserve
1968 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1970 Sends a notification to the patron that their hold has been filled (through
1971 ModReserveAffect, _not_ ModReserveFill)
1973 The letter code for this notice may be found using the following query:
1975 select distinct letter_code
1976 from message_transports
1977 inner join message_attributes using (message_attribute_id)
1978 where message_name = 'Hold_Filled'
1980 This will probably sipmly be 'HOLD', but because it is defined in the database,
1981 it is subject to addition or change.
1983 The following tables are availalbe witin the notice:
1994 sub _koha_notify_reserve {
1995 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1997 my $dbh = C4::Context->dbh;
1998 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
2000 # Try to get the borrower's email address
2001 my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
2003 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
2004 borrowernumber => $borrowernumber,
2005 message_name => 'Hold_Filled'
2008 my $sth = $dbh->prepare("
2011 WHERE borrowernumber = ?
2012 AND biblionumber = ?
2014 $sth->execute( $borrowernumber, $biblionumber );
2015 my $reserve = $sth->fetchrow_hashref;
2016 my $library = Koha::Libraries->find( $reserve->{branchcode} )->unblessed;
2018 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
2020 my %letter_params = (
2021 module => 'reserves',
2022 branchcode => $reserve->{branchcode},
2024 'branches' => $library,
2025 'borrowers' => $borrower,
2026 'biblio' => $biblionumber,
2027 'biblioitems' => $biblionumber,
2028 'reserves' => $reserve,
2029 'items', $reserve->{'itemnumber'},
2031 substitute => { today => output_pref( { dt => dt_from_string, dateonly => 1 } ) },
2034 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.
2035 my $send_notification = sub {
2036 my ( $mtt, $letter_code ) = (@_);
2037 return unless defined $letter_code;
2038 $letter_params{letter_code} = $letter_code;
2039 $letter_params{message_transport_type} = $mtt;
2040 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
2042 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
2046 C4::Letters::EnqueueLetter( {
2048 borrowernumber => $borrowernumber,
2049 from_address => $admin_email_address,
2050 message_transport_type => $mtt,
2054 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
2056 ( $mtt eq 'email' and not $to_address ) # No email address
2057 or ( $mtt eq 'sms' and not $borrower->{smsalertnumber} ) # No SMS number
2058 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
2061 &$send_notification($mtt, $letter_code);
2062 $notification_sent++;
2064 #Making sure that a print notification is sent if no other transport types can be utilized.
2065 if (! $notification_sent) {
2066 &$send_notification('print', 'HOLD');
2071 =head2 _ShiftPriorityByDateAndPriority
2073 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
2075 This increments the priority of all reserves after the one
2076 with either the lowest date after C<$reservedate>
2077 or the lowest priority after C<$priority>.
2079 It effectively makes room for a new reserve to be inserted with a certain
2080 priority, which is returned.
2082 This is most useful when the reservedate can be set by the user. It allows
2083 the new reserve to be placed before other reserves that have a later
2084 reservedate. Since priority also is set by the form in reserves/request.pl
2085 the sub accounts for that too.
2089 sub _ShiftPriorityByDateAndPriority {
2090 my ( $biblio, $resdate, $new_priority ) = @_;
2092 my $dbh = C4::Context->dbh;
2093 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
2094 my $sth = $dbh->prepare( $query );
2095 $sth->execute( $biblio, $resdate, $new_priority );
2096 my $min_priority = $sth->fetchrow;
2097 # if no such matches are found, $new_priority remains as original value
2098 $new_priority = $min_priority if ( $min_priority );
2100 # Shift the priority up by one; works in conjunction with the next SQL statement
2101 $query = "UPDATE reserves
2102 SET priority = priority+1
2103 WHERE biblionumber = ?
2104 AND borrowernumber = ?
2107 my $sth_update = $dbh->prepare( $query );
2109 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
2110 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
2111 $sth = $dbh->prepare( $query );
2112 $sth->execute( $new_priority, $biblio );
2113 while ( my $row = $sth->fetchrow_hashref ) {
2114 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2117 return $new_priority; # so the caller knows what priority they wind up receiving
2120 =head2 OPACItemHoldsAllowed
2122 OPACItemHoldsAllowed($item_record,$borrower_record);
2124 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see
2125 if specific item holds are allowed, returns true if so.
2129 sub OPACItemHoldsAllowed {
2130 my ($item,$borrower) = @_;
2132 my $branchcode = $item->{homebranch} or die "No homebranch";
2134 my $dbh = C4::Context->dbh;
2135 if (C4::Context->preference('item-level_itypes')) {
2136 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
2137 # When GetItem is fixed, we can remove this
2138 $itype = $item->{itype};
2141 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
2142 my $sth = $dbh->prepare($query);
2143 $sth->execute($item->{biblioitemnumber});
2144 if (my $data = $sth->fetchrow_hashref()){
2145 $itype = $data->{itemtype};
2149 my $query = "SELECT opacitemholds,categorycode,itemtype,branchcode FROM issuingrules WHERE
2150 (issuingrules.categorycode = ? OR issuingrules.categorycode = '*')
2152 (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2154 (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')
2156 issuingrules.categorycode desc,
2157 issuingrules.itemtype desc,
2158 issuingrules.branchcode desc
2160 my $sth = $dbh->prepare($query);
2161 $sth->execute($borrower->{categorycode},$itype,$branchcode);
2162 my $data = $sth->fetchrow_hashref;
2163 my $opacitemholds = uc substr ($data->{opacitemholds}, 0, 1);
2164 return '' if $opacitemholds eq 'N';
2165 return $opacitemholds;
2170 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2172 Use when checking out an item to handle reserves
2173 If $cancelreserve boolean is set to true, it will remove existing reserve
2178 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2180 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2181 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
2184 my $biblionumber = $res->{biblionumber};
2185 my $biblioitemnumber = $res->{biblioitemnumber};
2187 if ($res->{borrowernumber} == $borrowernumber) {
2188 ModReserveFill($res);
2192 # The item is reserved by someone else.
2193 # Find this item in the reserves
2196 foreach (@$all_reserves) {
2197 $_->{'borrowernumber'} == $borrowernumber or next;
2198 $_->{'biblionumber'} == $biblionumber or next;
2205 # The item is reserved by the current patron
2206 ModReserveFill($borr_res);
2209 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2210 RevertWaitingStatus({ itemnumber => $itemnumber });
2212 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2213 CancelReserve( { reserve_id => $res->{'reserve_id'} } );
2220 MergeHolds($dbh,$to_biblio, $from_biblio);
2222 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2227 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2228 my $sth = $dbh->prepare(
2229 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2231 $sth->execute($from_biblio);
2232 if ( my $data = $sth->fetchrow_hashref() ) {
2234 # holds exist on old record, if not we don't need to do anything
2235 $sth = $dbh->prepare(
2236 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2237 $sth->execute( $to_biblio, $from_biblio );
2240 # don't reorder those already waiting
2242 $sth = $dbh->prepare(
2243 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2245 my $upd_sth = $dbh->prepare(
2246 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2247 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
2249 $sth->execute( $to_biblio, 'W', 'T' );
2251 while ( my $reserve = $sth->fetchrow_hashref() ) {
2253 $priority, $to_biblio,
2254 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2255 $reserve->{'itemnumber'}
2262 =head2 RevertWaitingStatus
2264 RevertWaitingStatus({ itemnumber => $itemnumber });
2266 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2268 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2269 item level hold, even if it was only a bibliolevel hold to
2270 begin with. This is because we can no longer know if a hold
2271 was item-level or bib-level after a hold has been set to
2276 sub RevertWaitingStatus {
2277 my ( $params ) = @_;
2278 my $itemnumber = $params->{'itemnumber'};
2280 return unless ( $itemnumber );
2282 my $dbh = C4::Context->dbh;
2284 ## Get the waiting reserve we want to revert
2286 SELECT * FROM reserves
2287 WHERE itemnumber = ?
2288 AND found IS NOT NULL
2290 my $sth = $dbh->prepare( $query );
2291 $sth->execute( $itemnumber );
2292 my $reserve = $sth->fetchrow_hashref();
2294 ## Increment the priority of all other non-waiting
2295 ## reserves for this bib record
2299 priority = priority + 1
2305 $sth = $dbh->prepare( $query );
2306 $sth->execute( $reserve->{'biblionumber'} );
2308 ## Fix up the currently waiting reserve
2318 $sth = $dbh->prepare( $query );
2319 $sth->execute( $reserve->{'reserve_id'} );
2320 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2325 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2327 Returnes the first reserve id that matches the given criteria
2332 my ( $params ) = @_;
2334 return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2336 my $dbh = C4::Context->dbh();
2338 my $sql = "SELECT reserve_id FROM reserves WHERE ";
2342 foreach my $key ( keys %$params ) {
2343 if ( defined( $params->{$key} ) ) {
2344 push( @limits, "$key = ?" );
2345 push( @params, $params->{$key} );
2349 $sql .= join( " AND ", @limits );
2351 my $sth = $dbh->prepare( $sql );
2352 $sth->execute( @params );
2353 my $row = $sth->fetchrow_hashref();
2355 return $row->{'reserve_id'};
2360 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2362 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2364 The letter code will be HOLD_SLIP, and the following tables are
2365 available within the slip:
2377 my ($branch, $borrowernumber, $biblionumber) = @_;
2379 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2381 my $reserve_id = GetReserveId({
2382 biblionumber => $biblionumber,
2383 borrowernumber => $borrowernumber
2385 my $reserve = GetReserveInfo($reserve_id) or return;
2387 return C4::Letters::GetPreparedLetter (
2388 module => 'circulation',
2389 letter_code => 'HOLD_SLIP',
2390 branchcode => $branch,
2392 'reserves' => $reserve,
2393 'branches' => $reserve->{branchcode},
2394 'borrowers' => $reserve->{borrowernumber},
2395 'biblio' => $reserve->{biblionumber},
2396 'biblioitems' => $reserve->{biblionumber},
2397 'items' => $reserve->{itemnumber},
2402 =head2 GetReservesControlBranch
2404 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2406 Return the branchcode to be used to determine which reserves
2407 policy applies to a transaction.
2409 C<$item> is a hashref for an item. Only 'homebranch' is used.
2411 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2415 sub GetReservesControlBranch {
2416 my ( $item, $borrower ) = @_;
2418 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2421 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2422 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2428 =head2 CalculatePriority
2430 my $p = CalculatePriority($biblionumber, $resdate);
2432 Calculate priority for a new reserve on biblionumber, placing it at
2433 the end of the line of all holds whose start date falls before
2434 the current system time and that are neither on the hold shelf
2437 The reserve date parameter is optional; if it is supplied, the
2438 priority is based on the set of holds whose start date falls before
2439 the parameter value.
2441 After calculation of this priority, it is recommended to call
2442 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2447 sub CalculatePriority {
2448 my ( $biblionumber, $resdate ) = @_;
2451 SELECT COUNT(*) FROM reserves
2452 WHERE biblionumber = ?
2454 AND (found IS NULL OR found = '')
2456 #skip found==W or found==T (waiting or transit holds)
2458 $sql.= ' AND ( reservedate <= ? )';
2461 $sql.= ' AND ( reservedate < NOW() )';
2463 my $dbh = C4::Context->dbh();
2464 my @row = $dbh->selectrow_array(
2467 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2470 return @row ? $row[0]+1 : 1;
2473 =head2 IsItemOnHoldAndFound
2475 my $bool = IsItemFoundHold( $itemnumber );
2477 Returns true if the item is currently on hold
2478 and that hold has a non-null found status ( W, T, etc. )
2482 sub IsItemOnHoldAndFound {
2483 my ($itemnumber) = @_;
2485 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2487 my $found = $rs->count(
2489 itemnumber => $itemnumber,
2490 found => { '!=' => undef }
2499 Koha Development Team <http://koha-community.org/>