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;
46 use Koha::IssuingRules;
51 use List::MoreUtils qw( firstidx any );
55 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
59 C4::Reserves - Koha functions for dealing with reservation.
67 This modules provides somes functions to deal with reservations.
69 Reserves are stored in reserves table.
70 The following columns contains important values :
71 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
72 =0 : then the reserve is being dealed
73 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
74 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
75 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
76 F(inished) : the reserve has been completed, and is done
77 - itemnumber : empty : the reserve is still unaffected to an item
78 filled: the reserve is attached to an item
79 The complete workflow is :
80 ==== 1st use case ====
81 patron request a document, 1st available : P >0, F=NULL, I=NULL
82 a library having it run "transfertodo", and clic on the list
83 if there is no transfer to do, the reserve waiting
84 patron can pick it up P =0, F=W, I=filled
85 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
86 The pickup library receive the book, it check in P =0, F=W, I=filled
87 The patron borrow the book P =0, F=F, I=filled
89 ==== 2nd use case ====
90 patron requests a document, a given item,
91 If pickup is holding branch P =0, F=W, I=filled
92 If transfer needed, write in branchtransfer P =0, F=T, I=filled
93 The pickup library receive the book, it checks it in P =0, F=W, I=filled
94 The patron borrow the book P =0, F=F, I=filled
107 &GetReservesFromItemnumber
108 &GetReservesFromBorrowernumber
109 &GetReservesForBranch
122 &ModReserveMinusPriority
128 &CanReserveBeCanceledFromOpac
130 &CancelExpiredReserves
132 &AutoUnsuspendReserves
134 &IsAvailableForItemLevelRequest
136 &OPACItemHoldsAllowed
139 &ToggleLowestPriority
145 &GetReservesControlBranch
149 GetMaxPatronHoldsForRecord
151 @EXPORT_OK = qw( MergeHolds );
156 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
158 Adds reserve and generates HOLDPLACED message.
160 The following tables are available witin the HOLDPLACED message:
173 $branch, $borrowernumber, $biblionumber, $bibitems,
174 $priority, $resdate, $expdate, $notes,
175 $title, $checkitem, $found, $itemtype
178 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
179 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
181 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
183 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
185 # Make room in reserves for this before those of a later reserve date
186 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
191 # If the reserv had the waiting status, we had the value of the resdate
192 if ( $found eq 'W' ) {
193 $waitingdate = $resdate;
196 # Don't add itemtype limit if specific item is selected
197 $itemtype = undef if $checkitem;
199 # updates take place here
200 my $hold = Koha::Hold->new(
202 borrowernumber => $borrowernumber,
203 biblionumber => $biblionumber,
204 reservedate => $resdate,
205 branchcode => $branch,
206 priority => $priority,
207 reservenotes => $notes,
208 itemnumber => $checkitem,
210 waitingdate => $waitingdate,
211 expirationdate => $expdate,
212 itemtype => $itemtype,
216 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
217 if C4::Context->preference('HoldsLog');
219 my $reserve_id = $hold->id();
221 # add a reserve fee if needed
222 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
223 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
224 ChargeReserveFee( $borrowernumber, $reserve_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,
243 'reserves' => $hold->unblessed,
247 my $admin_email_address = $library->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
249 C4::Letters::EnqueueLetter(
251 borrowernumber => $borrowernumber,
252 message_transport_type => 'email',
253 from_address => $admin_email_address,
254 to_address => $admin_email_address,
265 $res = GetReserve( $reserve_id );
267 Return the current reserve.
272 my ($reserve_id) = @_;
274 my $dbh = C4::Context->dbh;
276 my $query = "SELECT * FROM reserves WHERE reserve_id = ?";
277 my $sth = $dbh->prepare( $query );
278 $sth->execute( $reserve_id );
279 return $sth->fetchrow_hashref();
282 =head2 GetReservesFromItemnumber
284 ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $waitingdate ) = GetReservesFromItemnumber($itemnumber);
286 Get the first reserve for a specific item number (based on priority). Returns the abovementioned values for that reserve.
288 The routine does not look at future reserves (read: item level holds), but DOES include future waits (a confirmed future hold).
292 sub GetReservesFromItemnumber {
293 my ($itemnumber) = @_;
295 my $schema = Koha::Database->new()->schema();
297 my $r = $schema->resultset('Reserve')->search(
299 itemnumber => $itemnumber,
302 reservedate => \'<= CAST( NOW() AS DATE )',
303 waitingdate => { '!=', undef }
307 order_by => 'priority',
315 $r->get_column('borrowernumber'),
316 $r->get_column('branchcode'),
322 =head2 GetReservesFromBorrowernumber
324 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
330 sub GetReservesFromBorrowernumber {
331 my ( $borrowernumber, $status ) = @_;
332 my $dbh = C4::Context->dbh;
335 $sth = $dbh->prepare("
338 WHERE borrowernumber=?
342 $sth->execute($borrowernumber,$status);
344 $sth = $dbh->prepare("
347 WHERE borrowernumber=?
350 $sth->execute($borrowernumber);
352 my $data = $sth->fetchall_arrayref({});
356 =head2 CanBookBeReserved
358 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber)
359 if ($canReserve eq 'OK') { #We can reserve this Item! }
361 See CanItemBeReserved() for possible return values.
365 sub CanBookBeReserved{
366 my ($borrowernumber, $biblionumber) = @_;
368 my $items = GetItemnumbersForBiblio($biblionumber);
369 #get items linked via host records
370 my @hostitems = get_hostitemnumbers_of($biblionumber);
372 push (@$items,@hostitems);
376 foreach my $item (@$items) {
377 $canReserve = CanItemBeReserved( $borrowernumber, $item );
378 return 'OK' if $canReserve eq 'OK';
383 =head2 CanItemBeReserved
385 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber)
386 if ($canReserve eq 'OK') { #We can reserve this Item! }
388 @RETURNS OK, if the Item can be reserved.
389 ageRestricted, if the Item is age restricted for this borrower.
390 damaged, if the Item is damaged.
391 cannotReserveFromOtherBranches, if syspref 'canreservefromotherbranches' is OK.
392 tooManyReserves, if the borrower has exceeded his maximum reserve amount.
393 notReservable, if holds on this item are not allowed
397 sub CanItemBeReserved {
398 my ( $borrowernumber, $itemnumber ) = @_;
400 my $dbh = C4::Context->dbh;
401 my $ruleitemtype; # itemtype of the matching issuing rule
402 my $allowedreserves = 0; # Total number of holds allowed across all records
403 my $holds_per_record = 1; # Total number of holds allowed for this one given record
405 # we retrieve borrowers and items informations #
406 # item->{itype} will come for biblioitems if necessery
407 my $item = GetItem($itemnumber);
408 my $biblioData = C4::Biblio::GetBiblioData( $item->{biblionumber} );
409 my $borrower = C4::Members::GetMember( 'borrowernumber' => $borrowernumber );
411 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
413 if ( $item->{damaged}
414 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
416 # Check for the age restriction
417 my ( $ageRestriction, $daysToAgeRestriction ) =
418 C4::Circulation::GetAgeRestriction( $biblioData->{agerestriction}, $borrower );
419 return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0;
421 # Check that the patron doesn't have an item level hold on this item already
422 return 'itemAlreadyOnHold'
423 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
425 my $controlbranch = C4::Context->preference('ReservesControlBranch');
428 SELECT count(*) AS count
430 LEFT JOIN items USING (itemnumber)
431 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
432 LEFT JOIN borrowers USING (borrowernumber)
433 WHERE borrowernumber = ?
437 my $branchfield = "reserves.branchcode";
439 if ( $controlbranch eq "ItemHomeLibrary" ) {
440 $branchfield = "items.homebranch";
441 $branchcode = $item->{homebranch};
443 elsif ( $controlbranch eq "PatronLibrary" ) {
444 $branchfield = "borrowers.branchcode";
445 $branchcode = $borrower->{branchcode};
449 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) {
450 $ruleitemtype = $rights->{itemtype};
451 $allowedreserves = $rights->{reservesallowed};
452 $holds_per_record = $rights->{holds_per_record};
458 $item = Koha::Items->find( $itemnumber );
459 my $holds = Koha::Holds->search(
461 borrowernumber => $borrowernumber,
462 biblionumber => $item->biblionumber,
463 found => undef, # Found holds don't count against a patron's holds limit
466 if ( $holds->count() >= $holds_per_record ) {
467 return "tooManyHoldsForThisRecord";
472 $querycount .= "AND $branchfield = ?";
474 # If using item-level itypes, fall back to the record
475 # level itemtype if the hold has no associated item
477 C4::Context->preference('item-level_itypes')
478 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
479 : " AND biblioitems.itemtype = ?"
480 if ( $ruleitemtype ne "*" );
482 my $sthcount = $dbh->prepare($querycount);
484 if ( $ruleitemtype eq "*" ) {
485 $sthcount->execute( $borrowernumber, $branchcode );
488 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
491 my $reservecount = "0";
492 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
493 $reservecount = $rowcount->{count};
496 # we check if it's ok or not
497 if ( $reservecount >= $allowedreserves ) {
498 return 'tooManyReserves';
501 my $circ_control_branch =
502 C4::Circulation::_GetCircControlBranch( $item->unblessed(), $borrower );
504 C4::Circulation::GetBranchItemRule( $circ_control_branch, $item->itype );
506 if ( $branchitemrule->{holdallowed} == 0 ) {
507 return 'notReservable';
510 if ( $branchitemrule->{holdallowed} == 1
511 && $borrower->{branchcode} ne $item->homebranch )
513 return 'cannotReserveFromOtherBranches';
516 # If reservecount is ok, we check item branch if IndependentBranches is ON
517 # and canreservefromotherbranches is OFF
518 if ( C4::Context->preference('IndependentBranches')
519 and !C4::Context->preference('canreservefromotherbranches') )
521 my $itembranch = $item->homebranch;
522 if ( $itembranch ne $borrower->{branchcode} ) {
523 return 'cannotReserveFromOtherBranches';
530 =head2 CanReserveBeCanceledFromOpac
532 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
534 returns 1 if reserve can be cancelled by user from OPAC.
535 First check if reserve belongs to user, next checks if reserve is not in
536 transfer or waiting status
540 sub CanReserveBeCanceledFromOpac {
541 my ($reserve_id, $borrowernumber) = @_;
543 return unless $reserve_id and $borrowernumber;
544 my $reserve = GetReserve($reserve_id);
546 return 0 unless $reserve->{borrowernumber} == $borrowernumber;
547 return 0 if ( $reserve->{found} eq 'W' ) or ( $reserve->{found} eq 'T' );
553 =head2 GetReserveCount
555 $number = &GetReserveCount($borrowernumber);
557 this function returns the number of reservation for a borrower given on input arg.
561 sub GetReserveCount {
562 my ($borrowernumber) = @_;
564 my $dbh = C4::Context->dbh;
567 SELECT COUNT(*) AS counter
569 WHERE borrowernumber = ?
571 my $sth = $dbh->prepare($query);
572 $sth->execute($borrowernumber);
573 my $row = $sth->fetchrow_hashref;
574 return $row->{counter};
577 =head2 GetOtherReserves
579 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
581 Check queued list of this document and check if this document must be transferred
585 sub GetOtherReserves {
586 my ($itemnumber) = @_;
589 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
590 if ($checkreserves) {
591 my $iteminfo = GetItem($itemnumber);
592 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
593 $messages->{'transfert'} = $checkreserves->{'branchcode'};
594 #minus priorities of others reservs
595 ModReserveMinusPriority(
597 $checkreserves->{'reserve_id'},
600 #launch the subroutine dotransfer
601 C4::Items::ModItemTransfer(
603 $iteminfo->{'holdingbranch'},
604 $checkreserves->{'branchcode'}
609 #step 2b : case of a reservation on the same branch, set the waiting status
611 $messages->{'waiting'} = 1;
612 ModReserveMinusPriority(
614 $checkreserves->{'reserve_id'},
616 ModReserveStatus($itemnumber,'W');
619 $nextreservinfo = $checkreserves->{'borrowernumber'};
622 return ( $messages, $nextreservinfo );
625 =head2 ChargeReserveFee
627 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
629 Charge the fee for a reserve (if $fee > 0)
633 sub ChargeReserveFee {
634 my ( $borrowernumber, $fee, $title ) = @_;
635 return if !$fee || $fee==0; # the last test is needed to include 0.00
637 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
639 my $dbh = C4::Context->dbh;
640 my $nextacctno = &getnextacctno( $borrowernumber );
641 $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
646 $fee = GetReserveFee( $borrowernumber, $biblionumber );
648 Calculate the fee for a reserve (if applicable).
653 my ( $borrowernumber, $biblionumber ) = @_;
655 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
658 SELECT COUNT(*) FROM items
659 LEFT JOIN issues USING (itemnumber)
660 WHERE items.biblionumber=? AND issues.issue_id IS NULL
663 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
666 my $dbh = C4::Context->dbh;
667 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
668 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
669 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
670 # This is a reconstruction of the old code:
671 # Compare number of items with items issued, and optionally check holds
672 # If not all items are issued and there are no holds: charge no fee
673 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
674 my ( $notissued, $reserved );
675 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
678 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
679 ( $biblionumber, $borrowernumber ) );
680 $fee = 0 if $reserved == 0;
686 =head2 GetReservesToBranch
688 @transreserv = GetReservesToBranch( $frombranch );
690 Get reserve list for a given branch
694 sub GetReservesToBranch {
695 my ( $frombranch ) = @_;
696 my $dbh = C4::Context->dbh;
697 my $sth = $dbh->prepare(
698 "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
703 $sth->execute( $frombranch );
706 while ( my $data = $sth->fetchrow_hashref ) {
707 $transreserv[$i] = $data;
710 return (@transreserv);
713 =head2 GetReservesForBranch
715 @transreserv = GetReservesForBranch($frombranch);
719 sub GetReservesForBranch {
720 my ($frombranch) = @_;
721 my $dbh = C4::Context->dbh;
724 SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
729 $query .= " AND branchcode=? " if ( $frombranch );
730 $query .= "ORDER BY waitingdate" ;
732 my $sth = $dbh->prepare($query);
734 $sth->execute($frombranch);
741 while ( my $data = $sth->fetchrow_hashref ) {
742 $transreserv[$i] = $data;
745 return (@transreserv);
748 =head2 GetReserveStatus
750 $reservestatus = GetReserveStatus($itemnumber);
752 Takes an itemnumber and returns the status of the reserve placed on it.
753 If several reserves exist, the reserve with the lower priority is given.
757 ## FIXME: I don't think this does what it thinks it does.
758 ## It only ever checks the first reserve result, even though
759 ## multiple reserves for that bib can have the itemnumber set
760 ## the sub is only used once in the codebase.
761 sub GetReserveStatus {
762 my ($itemnumber) = @_;
764 my $dbh = C4::Context->dbh;
766 my ($sth, $found, $priority);
768 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
769 $sth->execute($itemnumber);
770 ($found, $priority) = $sth->fetchrow_array;
774 return 'Waiting' if $found eq 'W' and $priority == 0;
775 return 'Finished' if $found eq 'F';
778 return 'Reserved' if $priority > 0;
780 return ''; # empty string here will remove need for checking undef, or less log lines
785 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
786 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
787 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
789 Find a book in the reserves.
791 C<$itemnumber> is the book's item number.
792 C<$lookahead> is the number of days to look in advance for future reserves.
794 As I understand it, C<&CheckReserves> looks for the given item in the
795 reserves. If it is found, that's a match, and C<$status> is set to
798 Otherwise, it finds the most important item in the reserves with the
799 same biblio number as this book (I'm not clear on this) and returns it
800 with C<$status> set to C<Reserved>.
802 C<&CheckReserves> returns a two-element list:
804 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
806 C<$reserve> is the reserve item that matched. It is a
807 reference-to-hash whose keys are mostly the fields of the reserves
808 table in the Koha database.
813 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
814 my $dbh = C4::Context->dbh;
817 if (C4::Context->preference('item-level_itypes')){
819 SELECT items.biblionumber,
820 items.biblioitemnumber,
821 itemtypes.notforloan,
822 items.notforloan AS itemnotforloan,
828 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
829 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
834 SELECT items.biblionumber,
835 items.biblioitemnumber,
836 itemtypes.notforloan,
837 items.notforloan AS itemnotforloan,
843 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
844 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
849 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
850 $sth->execute($item);
853 $sth = $dbh->prepare("$select WHERE barcode = ?");
854 $sth->execute($barcode);
856 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
857 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
859 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
861 return unless $itemnumber; # bail if we got nothing.
863 # if item is not for loan it cannot be reserved either.....
864 # except where items.notforloan < 0 : This indicates the item is holdable.
865 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
867 # Find this item in the reserves
868 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
870 # $priority and $highest are used to find the most important item
871 # in the list returned by &_Findgroupreserve. (The lower $priority,
872 # the more important the item.)
873 # $highest is the most important item we've seen so far.
875 if (scalar @reserves) {
876 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
877 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
878 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
880 my $priority = 10000000;
881 foreach my $res (@reserves) {
882 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
883 return ( "Waiting", $res, \@reserves ); # Found it
887 my $local_hold_match;
889 if ($LocalHoldsPriority) {
890 $borrowerinfo = C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
891 $iteminfo = C4::Items::GetItem($itemnumber);
893 my $local_holds_priority_item_branchcode =
894 $iteminfo->{$LocalHoldsPriorityItemControl};
895 my $local_holds_priority_patron_branchcode =
896 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
898 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
899 ? $borrowerinfo->{branchcode}
902 $local_holds_priority_item_branchcode eq
903 $local_holds_priority_patron_branchcode;
906 # See if this item is more important than what we've got so far
907 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
908 $iteminfo ||= C4::Items::GetItem($itemnumber);
909 next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
910 $borrowerinfo ||= C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
911 my $branch = GetReservesControlBranch( $iteminfo, $borrowerinfo );
912 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
913 next if ($branchitemrule->{'holdallowed'} == 0);
914 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
915 next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
916 $priority = $res->{'priority'};
918 last if $local_hold_match;
924 # If we get this far, then no exact match was found.
925 # We return the most important (i.e. next) reservation.
927 $highest->{'itemnumber'} = $item;
928 return ( "Reserved", $highest, \@reserves );
934 =head2 CancelExpiredReserves
936 CancelExpiredReserves();
938 Cancels all reserves with an expiration date from before today.
942 sub CancelExpiredReserves {
944 # Cancel reserves that have passed their expiration date.
945 my $dbh = C4::Context->dbh;
946 my $sth = $dbh->prepare( "
947 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
948 AND expirationdate IS NOT NULL
953 while ( my $res = $sth->fetchrow_hashref() ) {
954 CancelReserve({ reserve_id => $res->{'reserve_id'} });
957 # Cancel reserves that have been waiting too long
958 if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
959 my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
960 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
962 my $today = dt_from_string();
964 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
965 $sth = $dbh->prepare( $query );
966 $sth->execute( $max_pickup_delay );
968 while ( my $res = $sth->fetchrow_hashref ) {
970 unless ( $cancel_on_holidays ) {
971 my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
972 my $is_holiday = $calendar->is_holiday( $today );
980 CancelReserve({ reserve_id => $res->{'reserve_id'}, charge_cancel_fee => 1 });
987 =head2 AutoUnsuspendReserves
989 AutoUnsuspendReserves();
991 Unsuspends all suspended reserves with a suspend_until date from before today.
995 sub AutoUnsuspendReserves {
996 my $today = dt_from_string();
998 my @holds = Koha::Holds->search( { suspend_until => { '<' => $today->ymd() } } );
1000 map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
1003 =head2 CancelReserve
1005 CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber, ] [ charge_cancel_fee => 1 ] });
1007 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.
1012 my ( $params ) = @_;
1014 my $reserve_id = $params->{'reserve_id'};
1015 # Filter out only the desired keys; this will insert undefined values for elements missing in
1016 # \%params, but GetReserveId filters them out anyway.
1017 $reserve_id = GetReserveId( { biblionumber => $params->{'biblionumber'}, borrowernumber => $params->{'borrowernumber'}, itemnumber => $params->{'itemnumber'} } ) unless ( $reserve_id );
1019 return unless ( $reserve_id );
1021 my $dbh = C4::Context->dbh;
1023 my $reserve = GetReserve( $reserve_id );
1026 my $hold = Koha::Holds->find( $reserve_id );
1027 logaction( 'HOLDS', 'CANCEL', $hold->reserve_id, Dumper($hold->unblessed) )
1028 if C4::Context->preference('HoldsLog');
1032 SET cancellationdate = now(),
1034 WHERE reserve_id = ?
1036 my $sth = $dbh->prepare($query);
1037 $sth->execute( $reserve_id );
1040 INSERT INTO old_reserves
1041 SELECT * FROM reserves
1042 WHERE reserve_id = ?
1044 $sth = $dbh->prepare($query);
1045 $sth->execute( $reserve_id );
1048 DELETE FROM reserves
1049 WHERE reserve_id = ?
1051 $sth = $dbh->prepare($query);
1052 $sth->execute( $reserve_id );
1054 # now fix the priority on the others....
1055 _FixPriority({ biblionumber => $reserve->{biblionumber} });
1057 # and, if desired, charge a cancel fee
1058 my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
1059 if ( $charge && $params->{'charge_cancel_fee'} ) {
1060 manualinvoice($reserve->{'borrowernumber'}, $reserve->{'itemnumber'}, '', 'HE', $charge);
1069 ModReserve({ rank => $rank,
1070 reserve_id => $reserve_id,
1071 branchcode => $branchcode
1072 [, itemnumber => $itemnumber ]
1073 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1076 Change a hold request's priority or cancel it.
1078 C<$rank> specifies the effect of the change. If C<$rank>
1079 is 'W' or 'n', nothing happens. This corresponds to leaving a
1080 request alone when changing its priority in the holds queue
1083 If C<$rank> is 'del', the hold request is cancelled.
1085 If C<$rank> is an integer greater than zero, the priority of
1086 the request is set to that value. Since priority != 0 means
1087 that the item is not waiting on the hold shelf, setting the
1088 priority to a non-zero value also sets the request's found
1089 status and waiting date to NULL.
1091 The optional C<$itemnumber> parameter is used only when
1092 C<$rank> is a non-zero integer; if supplied, the itemnumber
1093 of the hold request is set accordingly; if omitted, the itemnumber
1096 B<FIXME:> Note that the forgoing can have the effect of causing
1097 item-level hold requests to turn into title-level requests. This
1098 will be fixed once reserves has separate columns for requested
1099 itemnumber and supplying itemnumber.
1104 my ( $params ) = @_;
1106 my $rank = $params->{'rank'};
1107 my $reserve_id = $params->{'reserve_id'};
1108 my $branchcode = $params->{'branchcode'};
1109 my $itemnumber = $params->{'itemnumber'};
1110 my $suspend_until = $params->{'suspend_until'};
1111 my $borrowernumber = $params->{'borrowernumber'};
1112 my $biblionumber = $params->{'biblionumber'};
1114 return if $rank eq "W";
1115 return if $rank eq "n";
1117 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1118 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1120 if ( $rank eq "del" ) {
1121 CancelReserve({ reserve_id => $reserve_id });
1123 elsif ($rank =~ /^\d+/ and $rank > 0) {
1124 my $hold = Koha::Holds->find($reserve_id);
1125 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1126 if C4::Context->preference('HoldsLog');
1131 branchcode => $branchcode,
1132 itemnumber => $itemnumber,
1134 waitingdate => undef
1138 if ( defined( $suspend_until ) ) {
1139 if ( $suspend_until ) {
1140 $suspend_until = eval { dt_from_string( $suspend_until ) };
1141 $hold->suspend_hold( $suspend_until );
1143 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1144 # If the hold is not suspended, this does nothing.
1145 $hold->set( { suspend_until => undef } )->store();
1149 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1153 =head2 ModReserveFill
1155 &ModReserveFill($reserve);
1157 Fill a reserve. If I understand this correctly, this means that the
1158 reserved book has been found and given to the patron who reserved it.
1160 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1161 whose keys are fields from the reserves table in the Koha database.
1165 sub ModReserveFill {
1167 my $reserve_id = $res->{'reserve_id'};
1169 my $hold = Koha::Holds->find($reserve_id);
1171 # get the priority on this record....
1172 my $priority = $hold->priority;
1174 # update the hold statuses, no need to store it though, we will be deleting it anyway
1182 Koha::Old::Hold->new( $hold->unblessed() )->store();
1186 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1187 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1188 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1191 # now fix the priority on the others (if the priority wasn't
1192 # already sorted!)....
1193 unless ( $priority == 0 ) {
1194 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1198 =head2 ModReserveStatus
1200 &ModReserveStatus($itemnumber, $newstatus);
1202 Update the reserve status for the active (priority=0) reserve.
1204 $itemnumber is the itemnumber the reserve is on
1206 $newstatus is the new status.
1210 sub ModReserveStatus {
1212 #first : check if we have a reservation for this item .
1213 my ($itemnumber, $newstatus) = @_;
1214 my $dbh = C4::Context->dbh;
1216 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1217 my $sth_set = $dbh->prepare($query);
1218 $sth_set->execute( $newstatus, $itemnumber );
1220 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1221 CartToShelf( $itemnumber );
1225 =head2 ModReserveAffect
1227 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1229 This function affect an item and a status for a given reserve, either fetched directly
1230 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1231 is given, only first reserve returned is affected, which is ok for anything but
1234 if $transferToDo is not set, then the status is set to "Waiting" as well.
1235 otherwise, a transfer is on the way, and the end of the transfer will
1236 take care of the waiting status
1240 sub ModReserveAffect {
1241 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1242 my $dbh = C4::Context->dbh;
1244 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1245 # attached to $itemnumber
1246 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1247 $sth->execute($itemnumber);
1248 my ($biblionumber) = $sth->fetchrow;
1250 # get request - need to find out if item is already
1251 # waiting in order to not send duplicate hold filled notifications
1254 # Find hold by id if we have it
1255 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1256 # Find item level hold for this item if there is one
1257 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1258 # Find record level hold if there is no item level hold
1259 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1261 return unless $hold;
1263 $reserve_id = $hold->id();
1265 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1267 # If we affect a reserve that has to be transferred, don't set to Waiting
1269 if ($transferToDo) {
1273 itemnumber => $itemnumber,
1279 # affect the reserve to Waiting as well.
1283 itemnumber => $itemnumber,
1285 waitingdate => dt_from_string(),
1291 _koha_notify_reserve( $hold->reserve_id )
1292 if ( !$transferToDo && !$already_on_shelf );
1294 _FixPriority( { biblionumber => $biblionumber } );
1296 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1297 CartToShelf($itemnumber);
1303 =head2 ModReserveCancelAll
1305 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1307 function to cancel reserv,check other reserves, and transfer document if it's necessary
1311 sub ModReserveCancelAll {
1314 my ( $itemnumber, $borrowernumber ) = @_;
1316 #step 1 : cancel the reservation
1317 my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1319 #step 2 launch the subroutine of the others reserves
1320 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1322 return ( $messages, $nextreservinfo );
1325 =head2 ModReserveMinusPriority
1327 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1329 Reduce the values of queued list
1333 sub ModReserveMinusPriority {
1334 my ( $itemnumber, $reserve_id ) = @_;
1336 #first step update the value of the first person on reserv
1337 my $dbh = C4::Context->dbh;
1340 SET priority = 0 , itemnumber = ?
1341 WHERE reserve_id = ?
1343 my $sth_upd = $dbh->prepare($query);
1344 $sth_upd->execute( $itemnumber, $reserve_id );
1345 # second step update all others reserves
1346 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1349 =head2 GetReserveInfo
1351 &GetReserveInfo($reserve_id);
1353 Get item and borrower details for a current hold.
1354 Current implementation this query should have a single result.
1358 sub GetReserveInfo {
1359 my ( $reserve_id ) = @_;
1360 my $dbh = C4::Context->dbh;
1365 reserves.borrowernumber,
1366 reserves.biblionumber,
1367 reserves.branchcode,
1368 reserves.waitingdate,
1384 items.holdingbranch,
1385 items.itemcallnumber,
1391 LEFT JOIN items USING(itemnumber)
1392 LEFT JOIN borrowers USING(borrowernumber)
1393 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1394 WHERE reserves.reserve_id = ?";
1395 my $sth = $dbh->prepare($strsth);
1396 $sth->execute($reserve_id);
1398 my $data = $sth->fetchrow_hashref;
1402 =head2 IsAvailableForItemLevelRequest
1404 my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1406 Checks whether a given item record is available for an
1407 item-level hold request. An item is available if
1409 * it is not lost AND
1410 * it is not damaged AND
1411 * it is not withdrawn AND
1412 * does not have a not for loan value > 0
1414 Need to check the issuingrules onshelfholds column,
1415 if this is set items on the shelf can be placed on hold
1417 Note that IsAvailableForItemLevelRequest() does not
1418 check if the staff operator is authorized to place
1419 a request on the item - in particular,
1420 this routine does not check IndependentBranches
1421 and canreservefromotherbranches.
1425 sub IsAvailableForItemLevelRequest {
1427 my $borrower = shift;
1429 my $dbh = C4::Context->dbh;
1430 # must check the notforloan setting of the itemtype
1431 # FIXME - a lot of places in the code do this
1432 # or something similar - need to be
1434 my $itype = _get_itype($item);
1435 my $notforloan_per_itemtype
1436 = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1440 $notforloan_per_itemtype ||
1441 $item->{itemlost} ||
1442 $item->{notforloan} > 0 ||
1443 $item->{withdrawn} ||
1444 ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1446 my $on_shelf_holds = _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1448 if ( $on_shelf_holds == 1 ) {
1450 } elsif ( $on_shelf_holds == 2 ) {
1452 Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1454 my $any_available = 0;
1456 foreach my $i (@items) {
1459 || $i->notforloan > 0
1462 || IsItemOnHoldAndFound( $i->id )
1464 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1465 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan;
1468 return $any_available ? 0 : 1;
1471 return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting";
1474 =head2 OnShelfHoldsAllowed
1476 OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode);
1478 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf
1479 holds are allowed, returns true if so.
1483 sub OnShelfHoldsAllowed {
1484 my ($item, $borrower) = @_;
1486 my $itype = _get_itype($item);
1487 return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1494 if (C4::Context->preference('item-level_itypes')) {
1495 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1496 # When GetItem is fixed, we can remove this
1497 $itype = $item->{itype};
1500 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1501 # So if we already have a biblioitems join when calling this function,
1502 # we don't need to access the database again
1503 $itype = $item->{itemtype};
1506 my $dbh = C4::Context->dbh;
1507 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1508 my $sth = $dbh->prepare($query);
1509 $sth->execute($item->{biblioitemnumber});
1510 if (my $data = $sth->fetchrow_hashref()){
1511 $itype = $data->{itemtype};
1517 sub _OnShelfHoldsAllowed {
1518 my ($itype,$borrowercategory,$branchcode) = @_;
1520 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ categorycode => $borrowercategory, itemtype => $itype, branchcode => $branchcode });
1521 return $issuing_rule ? $issuing_rule->onshelfholds : undef;
1524 =head2 AlterPriority
1526 AlterPriority( $where, $reserve_id );
1528 This function changes a reserve's priority up, down, to the top, or to the bottom.
1529 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1534 my ( $where, $reserve_id ) = @_;
1536 my $reserve = GetReserve( $reserve_id );
1538 if ( $reserve->{cancellationdate} ) {
1539 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1543 if ( $where eq 'up' || $where eq 'down' ) {
1545 my $priority = $reserve->{'priority'};
1546 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1547 _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1549 } elsif ( $where eq 'top' ) {
1551 _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1553 } elsif ( $where eq 'bottom' ) {
1555 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1560 =head2 ToggleLowestPriority
1562 ToggleLowestPriority( $borrowernumber, $biblionumber );
1564 This function sets the lowestPriority field to true if is false, and false if it is true.
1568 sub ToggleLowestPriority {
1569 my ( $reserve_id ) = @_;
1571 my $dbh = C4::Context->dbh;
1573 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1574 $sth->execute( $reserve_id );
1576 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1579 =head2 ToggleSuspend
1581 ToggleSuspend( $reserve_id );
1583 This function sets the suspend field to true if is false, and false if it is true.
1584 If the reserve is currently suspended with a suspend_until date, that date will
1585 be cleared when it is unsuspended.
1590 my ( $reserve_id, $suspend_until ) = @_;
1592 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1594 my $hold = Koha::Holds->find( $reserve_id );
1596 if ( $hold->is_suspended ) {
1599 $hold->suspend_hold( $suspend_until );
1606 borrowernumber => $borrowernumber,
1607 [ biblionumber => $biblionumber, ]
1608 [ suspend_until => $suspend_until, ]
1609 [ suspend => $suspend ]
1612 This function accepts a set of hash keys as its parameters.
1613 It requires either borrowernumber or biblionumber, or both.
1615 suspend_until is wholly optional.
1622 my $borrowernumber = $params{'borrowernumber'} || undef;
1623 my $biblionumber = $params{'biblionumber'} || undef;
1624 my $suspend_until = $params{'suspend_until'} || undef;
1625 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1627 $suspend_until = eval { dt_from_string($suspend_until) }
1628 if ( defined($suspend_until) );
1630 return unless ( $borrowernumber || $biblionumber );
1633 $params->{found} = undef;
1634 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1635 $params->{biblionumber} = $biblionumber if $biblionumber;
1637 my @holds = Koha::Holds->search($params);
1640 map { $_->suspend_hold($suspend_until) } @holds;
1643 map { $_->resume() } @holds;
1651 reserve_id => $reserve_id,
1653 [ignoreSetLowestRank => $ignoreSetLowestRank]
1658 _FixPriority({ biblionumber => $biblionumber});
1660 This routine adjusts the priority of a hold request and holds
1663 In the first form, where a reserve_id is passed, the priority of the
1664 hold is set to supplied rank, and other holds for that bib are adjusted
1665 accordingly. If the rank is "del", the hold is cancelled. If no rank
1666 is supplied, all of the holds on that bib have their priority adjusted
1667 as if the second form had been used.
1669 In the second form, where a biblionumber is passed, the holds on that
1670 bib (that are not captured) are sorted in order of increasing priority,
1671 then have reserves.priority set so that the first non-captured hold
1672 has its priority set to 1, the second non-captured hold has its priority
1673 set to 2, and so forth.
1675 In both cases, holds that have the lowestPriority flag on are have their
1676 priority adjusted to ensure that they remain at the end of the line.
1678 Note that the ignoreSetLowestRank parameter is meant to be used only
1679 when _FixPriority calls itself.
1684 my ( $params ) = @_;
1685 my $reserve_id = $params->{reserve_id};
1686 my $rank = $params->{rank} // '';
1687 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1688 my $biblionumber = $params->{biblionumber};
1690 my $dbh = C4::Context->dbh;
1692 unless ( $biblionumber ) {
1693 my $res = GetReserve( $reserve_id );
1694 $biblionumber = $res->{biblionumber};
1697 if ( $rank eq "del" ) {
1698 CancelReserve({ reserve_id => $reserve_id });
1700 elsif ( $rank eq "W" || $rank eq "0" ) {
1702 # make sure priority for waiting or in-transit items is 0
1706 WHERE reserve_id = ?
1707 AND found IN ('W', 'T')
1709 my $sth = $dbh->prepare($query);
1710 $sth->execute( $reserve_id );
1716 SELECT reserve_id, borrowernumber, reservedate
1718 WHERE biblionumber = ?
1719 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1720 ORDER BY priority ASC
1722 my $sth = $dbh->prepare($query);
1723 $sth->execute( $biblionumber );
1724 while ( my $line = $sth->fetchrow_hashref ) {
1725 push( @priority, $line );
1728 # To find the matching index
1730 my $key = -1; # to allow for 0 to be a valid result
1731 for ( $i = 0 ; $i < @priority ; $i++ ) {
1732 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1733 $key = $i; # save the index
1738 # if index exists in array then move it to new position
1739 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1740 my $new_rank = $rank -
1741 1; # $new_rank is what you want the new index to be in the array
1742 my $moving_item = splice( @priority, $key, 1 );
1743 splice( @priority, $new_rank, 0, $moving_item );
1746 # now fix the priority on those that are left....
1750 WHERE reserve_id = ?
1752 $sth = $dbh->prepare($query);
1753 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1756 $priority[$j]->{'reserve_id'}
1760 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1763 unless ( $ignoreSetLowestRank ) {
1764 while ( my $res = $sth->fetchrow_hashref() ) {
1766 reserve_id => $res->{'reserve_id'},
1768 ignoreSetLowestRank => 1
1774 =head2 _Findgroupreserve
1776 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1778 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1779 first match found. If neither, then we look for non-holds-queue based holds.
1780 Lookahead is the number of days to look in advance.
1782 C<&_Findgroupreserve> returns :
1783 C<@results> is an array of references-to-hash whose keys are mostly
1784 fields from the reserves table of the Koha database, plus
1785 C<biblioitemnumber>.
1789 sub _Findgroupreserve {
1790 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1791 my $dbh = C4::Context->dbh;
1793 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1794 # check for exact targeted match
1795 my $item_level_target_query = qq{
1796 SELECT reserves.biblionumber AS biblionumber,
1797 reserves.borrowernumber AS borrowernumber,
1798 reserves.reservedate AS reservedate,
1799 reserves.branchcode AS branchcode,
1800 reserves.cancellationdate AS cancellationdate,
1801 reserves.found AS found,
1802 reserves.reservenotes AS reservenotes,
1803 reserves.priority AS priority,
1804 reserves.timestamp AS timestamp,
1805 biblioitems.biblioitemnumber AS biblioitemnumber,
1806 reserves.itemnumber AS itemnumber,
1807 reserves.reserve_id AS reserve_id,
1808 reserves.itemtype AS itemtype
1810 JOIN biblioitems USING (biblionumber)
1811 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1814 AND item_level_request = 1
1816 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1820 my $sth = $dbh->prepare($item_level_target_query);
1821 $sth->execute($itemnumber, $lookahead||0);
1823 if ( my $data = $sth->fetchrow_hashref ) {
1824 push( @results, $data )
1825 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1827 return @results if @results;
1829 # check for title-level targeted match
1830 my $title_level_target_query = qq{
1831 SELECT reserves.biblionumber AS biblionumber,
1832 reserves.borrowernumber AS borrowernumber,
1833 reserves.reservedate AS reservedate,
1834 reserves.branchcode AS branchcode,
1835 reserves.cancellationdate AS cancellationdate,
1836 reserves.found AS found,
1837 reserves.reservenotes AS reservenotes,
1838 reserves.priority AS priority,
1839 reserves.timestamp AS timestamp,
1840 biblioitems.biblioitemnumber AS biblioitemnumber,
1841 reserves.itemnumber AS itemnumber,
1842 reserves.reserve_id AS reserve_id,
1843 reserves.itemtype AS itemtype
1845 JOIN biblioitems USING (biblionumber)
1846 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1849 AND item_level_request = 0
1850 AND hold_fill_targets.itemnumber = ?
1851 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1855 $sth = $dbh->prepare($title_level_target_query);
1856 $sth->execute($itemnumber, $lookahead||0);
1858 if ( my $data = $sth->fetchrow_hashref ) {
1859 push( @results, $data )
1860 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1862 return @results if @results;
1865 SELECT reserves.biblionumber AS biblionumber,
1866 reserves.borrowernumber AS borrowernumber,
1867 reserves.reservedate AS reservedate,
1868 reserves.waitingdate AS waitingdate,
1869 reserves.branchcode AS branchcode,
1870 reserves.cancellationdate AS cancellationdate,
1871 reserves.found AS found,
1872 reserves.reservenotes AS reservenotes,
1873 reserves.priority AS priority,
1874 reserves.timestamp AS timestamp,
1875 reserves.itemnumber AS itemnumber,
1876 reserves.reserve_id AS reserve_id,
1877 reserves.itemtype AS itemtype
1879 WHERE reserves.biblionumber = ?
1880 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1881 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1885 $sth = $dbh->prepare($query);
1886 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1888 while ( my $data = $sth->fetchrow_hashref ) {
1889 push( @results, $data )
1890 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1895 =head2 _koha_notify_reserve
1897 _koha_notify_reserve( $hold->reserve_id );
1899 Sends a notification to the patron that their hold has been filled (through
1900 ModReserveAffect, _not_ ModReserveFill)
1902 The letter code for this notice may be found using the following query:
1904 select distinct letter_code
1905 from message_transports
1906 inner join message_attributes using (message_attribute_id)
1907 where message_name = 'Hold_Filled'
1909 This will probably sipmly be 'HOLD', but because it is defined in the database,
1910 it is subject to addition or change.
1912 The following tables are availalbe witin the notice:
1923 sub _koha_notify_reserve {
1924 my $reserve_id = shift;
1925 my $hold = Koha::Holds->find($reserve_id);
1926 my $borrowernumber = $hold->borrowernumber;
1928 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1930 # Try to get the borrower's email address
1931 my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1933 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1934 borrowernumber => $borrowernumber,
1935 message_name => 'Hold_Filled'
1938 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1940 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1942 my %letter_params = (
1943 module => 'reserves',
1944 branchcode => $hold->branchcode,
1946 'branches' => $library,
1947 'borrowers' => $borrower,
1948 'biblio' => $hold->biblionumber,
1949 'biblioitems' => $hold->biblionumber,
1950 'reserves' => $hold->unblessed,
1951 'items' => $hold->itemnumber,
1953 substitute => { today => output_pref( { dt => dt_from_string, dateonly => 1 } ) },
1956 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.
1957 my $send_notification = sub {
1958 my ( $mtt, $letter_code ) = (@_);
1959 return unless defined $letter_code;
1960 $letter_params{letter_code} = $letter_code;
1961 $letter_params{message_transport_type} = $mtt;
1962 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1964 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1968 C4::Letters::EnqueueLetter( {
1970 borrowernumber => $borrowernumber,
1971 from_address => $admin_email_address,
1972 message_transport_type => $mtt,
1976 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1978 ( $mtt eq 'email' and not $to_address ) # No email address
1979 or ( $mtt eq 'sms' and not $borrower->{smsalertnumber} ) # No SMS number
1980 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1983 &$send_notification($mtt, $letter_code);
1984 $notification_sent++;
1986 #Making sure that a print notification is sent if no other transport types can be utilized.
1987 if (! $notification_sent) {
1988 &$send_notification('print', 'HOLD');
1993 =head2 _ShiftPriorityByDateAndPriority
1995 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1997 This increments the priority of all reserves after the one
1998 with either the lowest date after C<$reservedate>
1999 or the lowest priority after C<$priority>.
2001 It effectively makes room for a new reserve to be inserted with a certain
2002 priority, which is returned.
2004 This is most useful when the reservedate can be set by the user. It allows
2005 the new reserve to be placed before other reserves that have a later
2006 reservedate. Since priority also is set by the form in reserves/request.pl
2007 the sub accounts for that too.
2011 sub _ShiftPriorityByDateAndPriority {
2012 my ( $biblio, $resdate, $new_priority ) = @_;
2014 my $dbh = C4::Context->dbh;
2015 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
2016 my $sth = $dbh->prepare( $query );
2017 $sth->execute( $biblio, $resdate, $new_priority );
2018 my $min_priority = $sth->fetchrow;
2019 # if no such matches are found, $new_priority remains as original value
2020 $new_priority = $min_priority if ( $min_priority );
2022 # Shift the priority up by one; works in conjunction with the next SQL statement
2023 $query = "UPDATE reserves
2024 SET priority = priority+1
2025 WHERE biblionumber = ?
2026 AND borrowernumber = ?
2029 my $sth_update = $dbh->prepare( $query );
2031 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
2032 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
2033 $sth = $dbh->prepare( $query );
2034 $sth->execute( $new_priority, $biblio );
2035 while ( my $row = $sth->fetchrow_hashref ) {
2036 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2039 return $new_priority; # so the caller knows what priority they wind up receiving
2042 =head2 OPACItemHoldsAllowed
2044 OPACItemHoldsAllowed($item_record,$borrower_record);
2046 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see
2047 if specific item holds are allowed, returns true if so.
2051 sub OPACItemHoldsAllowed {
2052 my ($item,$borrower) = @_;
2054 my $branchcode = $item->{homebranch} or die "No homebranch";
2056 my $dbh = C4::Context->dbh;
2057 if (C4::Context->preference('item-level_itypes')) {
2058 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
2059 # When GetItem is fixed, we can remove this
2060 $itype = $item->{itype};
2063 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
2064 my $sth = $dbh->prepare($query);
2065 $sth->execute($item->{biblioitemnumber});
2066 if (my $data = $sth->fetchrow_hashref()){
2067 $itype = $data->{itemtype};
2071 my $query = "SELECT opacitemholds,categorycode,itemtype,branchcode FROM issuingrules WHERE
2072 (issuingrules.categorycode = ? OR issuingrules.categorycode = '*')
2074 (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2076 (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')
2078 issuingrules.categorycode desc,
2079 issuingrules.itemtype desc,
2080 issuingrules.branchcode desc
2082 my $sth = $dbh->prepare($query);
2083 $sth->execute($borrower->{categorycode},$itype,$branchcode);
2084 my $data = $sth->fetchrow_hashref;
2085 my $opacitemholds = uc substr ($data->{opacitemholds}, 0, 1);
2086 return '' if $opacitemholds eq 'N';
2087 return $opacitemholds;
2092 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2094 Use when checking out an item to handle reserves
2095 If $cancelreserve boolean is set to true, it will remove existing reserve
2100 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2102 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2103 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
2106 my $biblionumber = $res->{biblionumber};
2108 if ($res->{borrowernumber} == $borrowernumber) {
2109 ModReserveFill($res);
2113 # The item is reserved by someone else.
2114 # Find this item in the reserves
2117 foreach (@$all_reserves) {
2118 $_->{'borrowernumber'} == $borrowernumber or next;
2119 $_->{'biblionumber'} == $biblionumber or next;
2126 # The item is reserved by the current patron
2127 ModReserveFill($borr_res);
2130 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2131 RevertWaitingStatus({ itemnumber => $itemnumber });
2133 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2134 CancelReserve( { reserve_id => $res->{'reserve_id'} } );
2141 MergeHolds($dbh,$to_biblio, $from_biblio);
2143 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2148 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2149 my $sth = $dbh->prepare(
2150 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2152 $sth->execute($from_biblio);
2153 if ( my $data = $sth->fetchrow_hashref() ) {
2155 # holds exist on old record, if not we don't need to do anything
2156 $sth = $dbh->prepare(
2157 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2158 $sth->execute( $to_biblio, $from_biblio );
2161 # don't reorder those already waiting
2163 $sth = $dbh->prepare(
2164 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2166 my $upd_sth = $dbh->prepare(
2167 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2168 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
2170 $sth->execute( $to_biblio, 'W', 'T' );
2172 while ( my $reserve = $sth->fetchrow_hashref() ) {
2174 $priority, $to_biblio,
2175 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2176 $reserve->{'itemnumber'}
2183 =head2 RevertWaitingStatus
2185 RevertWaitingStatus({ itemnumber => $itemnumber });
2187 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2189 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2190 item level hold, even if it was only a bibliolevel hold to
2191 begin with. This is because we can no longer know if a hold
2192 was item-level or bib-level after a hold has been set to
2197 sub RevertWaitingStatus {
2198 my ( $params ) = @_;
2199 my $itemnumber = $params->{'itemnumber'};
2201 return unless ( $itemnumber );
2203 my $dbh = C4::Context->dbh;
2205 ## Get the waiting reserve we want to revert
2207 SELECT * FROM reserves
2208 WHERE itemnumber = ?
2209 AND found IS NOT NULL
2211 my $sth = $dbh->prepare( $query );
2212 $sth->execute( $itemnumber );
2213 my $reserve = $sth->fetchrow_hashref();
2215 ## Increment the priority of all other non-waiting
2216 ## reserves for this bib record
2220 priority = priority + 1
2226 $sth = $dbh->prepare( $query );
2227 $sth->execute( $reserve->{'biblionumber'} );
2229 ## Fix up the currently waiting reserve
2239 $sth = $dbh->prepare( $query );
2240 $sth->execute( $reserve->{'reserve_id'} );
2241 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2246 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2248 Returnes the first reserve id that matches the given criteria
2253 my ( $params ) = @_;
2255 return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2257 foreach my $key ( keys %$params ) {
2258 delete $params->{$key} unless defined( $params->{$key} );
2261 my $hold = Koha::Holds->search( $params )->next();
2263 return unless $hold;
2270 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2272 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2274 The letter code will be HOLD_SLIP, and the following tables are
2275 available within the slip:
2287 my ($branch, $borrowernumber, $biblionumber) = @_;
2289 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2291 my $reserve_id = GetReserveId({
2292 biblionumber => $biblionumber,
2293 borrowernumber => $borrowernumber
2295 my $reserve = GetReserveInfo($reserve_id) or return;
2297 return C4::Letters::GetPreparedLetter (
2298 module => 'circulation',
2299 letter_code => 'HOLD_SLIP',
2300 branchcode => $branch,
2302 'reserves' => $reserve,
2303 'branches' => $reserve->{branchcode},
2304 'borrowers' => $reserve->{borrowernumber},
2305 'biblio' => $reserve->{biblionumber},
2306 'biblioitems' => $reserve->{biblionumber},
2307 'items' => $reserve->{itemnumber},
2312 =head2 GetReservesControlBranch
2314 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2316 Return the branchcode to be used to determine which reserves
2317 policy applies to a transaction.
2319 C<$item> is a hashref for an item. Only 'homebranch' is used.
2321 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2325 sub GetReservesControlBranch {
2326 my ( $item, $borrower ) = @_;
2328 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2331 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2332 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2338 =head2 CalculatePriority
2340 my $p = CalculatePriority($biblionumber, $resdate);
2342 Calculate priority for a new reserve on biblionumber, placing it at
2343 the end of the line of all holds whose start date falls before
2344 the current system time and that are neither on the hold shelf
2347 The reserve date parameter is optional; if it is supplied, the
2348 priority is based on the set of holds whose start date falls before
2349 the parameter value.
2351 After calculation of this priority, it is recommended to call
2352 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2357 sub CalculatePriority {
2358 my ( $biblionumber, $resdate ) = @_;
2361 SELECT COUNT(*) FROM reserves
2362 WHERE biblionumber = ?
2364 AND (found IS NULL OR found = '')
2366 #skip found==W or found==T (waiting or transit holds)
2368 $sql.= ' AND ( reservedate <= ? )';
2371 $sql.= ' AND ( reservedate < NOW() )';
2373 my $dbh = C4::Context->dbh();
2374 my @row = $dbh->selectrow_array(
2377 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2380 return @row ? $row[0]+1 : 1;
2383 =head2 IsItemOnHoldAndFound
2385 my $bool = IsItemFoundHold( $itemnumber );
2387 Returns true if the item is currently on hold
2388 and that hold has a non-null found status ( W, T, etc. )
2392 sub IsItemOnHoldAndFound {
2393 my ($itemnumber) = @_;
2395 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2397 my $found = $rs->count(
2399 itemnumber => $itemnumber,
2400 found => { '!=' => undef }
2407 =head2 GetMaxPatronHoldsForRecord
2409 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2411 For multiple holds on a given record for a given patron, the max
2412 number of record level holds that a patron can be placed is the highest
2413 value of the holds_per_record rule for each item if the record for that
2414 patron. This subroutine finds and returns the highest holds_per_record
2415 rule value for a given patron id and record id.
2419 sub GetMaxPatronHoldsForRecord {
2420 my ( $borrowernumber, $biblionumber ) = @_;
2422 my $patron = Koha::Patrons->find($borrowernumber);
2423 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2425 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2427 my $categorycode = $patron->categorycode;
2429 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2432 foreach my $item (@items) {
2433 my $itemtype = $item->effective_itemtype();
2435 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2437 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2438 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2439 $max = $holds_per_record if $holds_per_record > $max;
2447 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2449 Returns the matching hold related issuingrule fields for a given
2450 patron category, itemtype, and library.
2455 my ( $categorycode, $itemtype, $branchcode ) = @_;
2457 my $dbh = C4::Context->dbh;
2459 my $sth = $dbh->prepare(
2461 SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record
2463 WHERE (categorycode in (?,'*') )
2464 AND (itemtype IN (?,'*'))
2465 AND (branchcode IN (?,'*'))
2466 ORDER BY categorycode DESC,
2472 $sth->execute( $categorycode, $itemtype, $branchcode );
2474 return $sth->fetchrow_hashref();
2479 Koha Development Team <http://koha-community.org/>