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;
47 use Koha::IssuingRules;
52 use List::MoreUtils qw( firstidx any );
56 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
60 C4::Reserves - Koha functions for dealing with reservation.
68 This modules provides somes functions to deal with reservations.
70 Reserves are stored in reserves table.
71 The following columns contains important values :
72 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
73 =0 : then the reserve is being dealed
74 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
75 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
76 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
77 F(inished) : the reserve has been completed, and is done
78 - itemnumber : empty : the reserve is still unaffected to an item
79 filled: the reserve is attached to an item
80 The complete workflow is :
81 ==== 1st use case ====
82 patron request a document, 1st available : P >0, F=NULL, I=NULL
83 a library having it run "transfertodo", and clic on the list
84 if there is no transfer to do, the reserve waiting
85 patron can pick it up P =0, F=W, I=filled
86 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
87 The pickup library receive the book, it check in P =0, F=W, I=filled
88 The patron borrow the book P =0, F=F, I=filled
90 ==== 2nd use case ====
91 patron requests a document, a given item,
92 If pickup is holding branch P =0, F=W, I=filled
93 If transfer needed, write in branchtransfer P =0, F=T, I=filled
94 The pickup library receive the book, it checks it in P =0, F=W, I=filled
95 The patron borrow the book P =0, F=F, I=filled
116 &ModReserveMinusPriority
122 &CanReserveBeCanceledFromOpac
123 &CancelExpiredReserves
125 &AutoUnsuspendReserves
127 &IsAvailableForItemLevelRequest
130 &ToggleLowestPriority
136 &GetReservesControlBranch
140 GetMaxPatronHoldsForRecord
142 @EXPORT_OK = qw( MergeHolds );
147 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
149 Adds reserve and generates HOLDPLACED message.
151 The following tables are available witin the HOLDPLACED message:
164 $branch, $borrowernumber, $biblionumber, $bibitems,
165 $priority, $resdate, $expdate, $notes,
166 $title, $checkitem, $found, $itemtype
169 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
170 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
172 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
174 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
176 # Make room in reserves for this before those of a later reserve date
177 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
182 # If the reserv had the waiting status, we had the value of the resdate
183 if ( $found eq 'W' ) {
184 $waitingdate = $resdate;
187 # Don't add itemtype limit if specific item is selected
188 $itemtype = undef if $checkitem;
190 # updates take place here
191 my $hold = Koha::Hold->new(
193 borrowernumber => $borrowernumber,
194 biblionumber => $biblionumber,
195 reservedate => $resdate,
196 branchcode => $branch,
197 priority => $priority,
198 reservenotes => $notes,
199 itemnumber => $checkitem,
201 waitingdate => $waitingdate,
202 expirationdate => $expdate,
203 itemtype => $itemtype,
206 $hold->set_waiting() if $found eq 'W';
208 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
209 if C4::Context->preference('HoldsLog');
211 my $reserve_id = $hold->id();
213 # add a reserve fee if needed
214 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
215 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
216 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
219 _FixPriority({ biblionumber => $biblionumber});
221 # Send e-mail to librarian if syspref is active
222 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
223 my $patron = Koha::Patrons->find( $borrowernumber );
224 my $library = $patron->library;
225 if ( my $letter = C4::Letters::GetPreparedLetter (
226 module => 'reserves',
227 letter_code => 'HOLDPLACED',
228 branchcode => $branch,
229 lang => $patron->lang,
231 'branches' => $library->unblessed,
232 'borrowers' => $patron->unblessed,
233 'biblio' => $biblionumber,
234 'biblioitems' => $biblionumber,
235 'items' => $checkitem,
236 'reserves' => $hold->unblessed,
240 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
242 C4::Letters::EnqueueLetter(
244 borrowernumber => $borrowernumber,
245 message_transport_type => 'email',
246 from_address => $admin_email_address,
247 to_address => $admin_email_address,
256 =head2 CanBookBeReserved
258 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber)
259 if ($canReserve eq 'OK') { #We can reserve this Item! }
261 See CanItemBeReserved() for possible return values.
265 sub CanBookBeReserved{
266 my ($borrowernumber, $biblionumber) = @_;
268 my $items = GetItemnumbersForBiblio($biblionumber);
269 #get items linked via host records
270 my @hostitems = get_hostitemnumbers_of($biblionumber);
272 push (@$items,@hostitems);
276 foreach my $item (@$items) {
277 $canReserve = CanItemBeReserved( $borrowernumber, $item );
278 return 'OK' if $canReserve eq 'OK';
283 =head2 CanItemBeReserved
285 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber)
286 if ($canReserve eq 'OK') { #We can reserve this Item! }
288 @RETURNS OK, if the Item can be reserved.
289 ageRestricted, if the Item is age restricted for this borrower.
290 damaged, if the Item is damaged.
291 cannotReserveFromOtherBranches, if syspref 'canreservefromotherbranches' is OK.
292 tooManyReserves, if the borrower has exceeded his maximum reserve amount.
293 notReservable, if holds on this item are not allowed
297 sub CanItemBeReserved {
298 my ( $borrowernumber, $itemnumber ) = @_;
300 my $dbh = C4::Context->dbh;
301 my $ruleitemtype; # itemtype of the matching issuing rule
302 my $allowedreserves = 0; # Total number of holds allowed across all records
303 my $holds_per_record = 1; # Total number of holds allowed for this one given record
305 # we retrieve borrowers and items informations #
306 # item->{itype} will come for biblioitems if necessery
307 my $item = GetItem($itemnumber);
308 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
309 my $patron = Koha::Patrons->find( $borrowernumber );
310 my $borrower = $patron->unblessed;
312 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
314 if ( $item->{damaged}
315 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
317 # Check for the age restriction
318 my ( $ageRestriction, $daysToAgeRestriction ) =
319 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
320 return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0;
322 # Check that the patron doesn't have an item level hold on this item already
323 return 'itemAlreadyOnHold'
324 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
326 my $controlbranch = C4::Context->preference('ReservesControlBranch');
329 SELECT count(*) AS count
331 LEFT JOIN items USING (itemnumber)
332 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
333 LEFT JOIN borrowers USING (borrowernumber)
334 WHERE borrowernumber = ?
338 my $branchfield = "reserves.branchcode";
340 if ( $controlbranch eq "ItemHomeLibrary" ) {
341 $branchfield = "items.homebranch";
342 $branchcode = $item->{homebranch};
344 elsif ( $controlbranch eq "PatronLibrary" ) {
345 $branchfield = "borrowers.branchcode";
346 $branchcode = $borrower->{branchcode};
350 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) {
351 $ruleitemtype = $rights->{itemtype};
352 $allowedreserves = $rights->{reservesallowed};
353 $holds_per_record = $rights->{holds_per_record};
359 $item = Koha::Items->find( $itemnumber );
360 my $holds = Koha::Holds->search(
362 borrowernumber => $borrowernumber,
363 biblionumber => $item->biblionumber,
364 found => undef, # Found holds don't count against a patron's holds limit
367 if ( $holds->count() >= $holds_per_record ) {
368 return "tooManyHoldsForThisRecord";
373 $querycount .= "AND $branchfield = ?";
375 # If using item-level itypes, fall back to the record
376 # level itemtype if the hold has no associated item
378 C4::Context->preference('item-level_itypes')
379 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
380 : " AND biblioitems.itemtype = ?"
381 if ( $ruleitemtype ne "*" );
383 my $sthcount = $dbh->prepare($querycount);
385 if ( $ruleitemtype eq "*" ) {
386 $sthcount->execute( $borrowernumber, $branchcode );
389 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
392 my $reservecount = "0";
393 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
394 $reservecount = $rowcount->{count};
397 # we check if it's ok or not
398 if ( $reservecount >= $allowedreserves ) {
399 return 'tooManyReserves';
402 my $circ_control_branch =
403 C4::Circulation::_GetCircControlBranch( $item->unblessed(), $borrower );
405 C4::Circulation::GetBranchItemRule( $circ_control_branch, $item->itype );
407 if ( $branchitemrule->{holdallowed} == 0 ) {
408 return 'notReservable';
411 if ( $branchitemrule->{holdallowed} == 1
412 && $borrower->{branchcode} ne $item->homebranch )
414 return 'cannotReserveFromOtherBranches';
417 # If reservecount is ok, we check item branch if IndependentBranches is ON
418 # and canreservefromotherbranches is OFF
419 if ( C4::Context->preference('IndependentBranches')
420 and !C4::Context->preference('canreservefromotherbranches') )
422 my $itembranch = $item->homebranch;
423 if ( $itembranch ne $borrower->{branchcode} ) {
424 return 'cannotReserveFromOtherBranches';
431 =head2 CanReserveBeCanceledFromOpac
433 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
435 returns 1 if reserve can be cancelled by user from OPAC.
436 First check if reserve belongs to user, next checks if reserve is not in
437 transfer or waiting status
441 sub CanReserveBeCanceledFromOpac {
442 my ($reserve_id, $borrowernumber) = @_;
444 return unless $reserve_id and $borrowernumber;
445 my $reserve = Koha::Holds->find($reserve_id);
447 return 0 unless $reserve->borrowernumber == $borrowernumber;
448 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
454 =head2 GetOtherReserves
456 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
458 Check queued list of this document and check if this document must be transferred
462 sub GetOtherReserves {
463 my ($itemnumber) = @_;
466 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
467 if ($checkreserves) {
468 my $iteminfo = GetItem($itemnumber);
469 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
470 $messages->{'transfert'} = $checkreserves->{'branchcode'};
471 #minus priorities of others reservs
472 ModReserveMinusPriority(
474 $checkreserves->{'reserve_id'},
477 #launch the subroutine dotransfer
478 C4::Items::ModItemTransfer(
480 $iteminfo->{'holdingbranch'},
481 $checkreserves->{'branchcode'}
486 #step 2b : case of a reservation on the same branch, set the waiting status
488 $messages->{'waiting'} = 1;
489 ModReserveMinusPriority(
491 $checkreserves->{'reserve_id'},
493 ModReserveStatus($itemnumber,'W');
496 $nextreservinfo = $checkreserves->{'borrowernumber'};
499 return ( $messages, $nextreservinfo );
502 =head2 ChargeReserveFee
504 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
506 Charge the fee for a reserve (if $fee > 0)
510 sub ChargeReserveFee {
511 my ( $borrowernumber, $fee, $title ) = @_;
512 return if !$fee || $fee==0; # the last test is needed to include 0.00
514 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
516 my $dbh = C4::Context->dbh;
517 my $nextacctno = &getnextacctno( $borrowernumber );
518 $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
523 $fee = GetReserveFee( $borrowernumber, $biblionumber );
525 Calculate the fee for a reserve (if applicable).
530 my ( $borrowernumber, $biblionumber ) = @_;
532 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
535 SELECT COUNT(*) FROM items
536 LEFT JOIN issues USING (itemnumber)
537 WHERE items.biblionumber=? AND issues.issue_id IS NULL
540 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
543 my $dbh = C4::Context->dbh;
544 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
545 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
546 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
547 # This is a reconstruction of the old code:
548 # Compare number of items with items issued, and optionally check holds
549 # If not all items are issued and there are no holds: charge no fee
550 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
551 my ( $notissued, $reserved );
552 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
555 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
556 ( $biblionumber, $borrowernumber ) );
557 $fee = 0 if $reserved == 0;
563 =head2 GetReserveStatus
565 $reservestatus = GetReserveStatus($itemnumber);
567 Takes an itemnumber and returns the status of the reserve placed on it.
568 If several reserves exist, the reserve with the lower priority is given.
572 ## FIXME: I don't think this does what it thinks it does.
573 ## It only ever checks the first reserve result, even though
574 ## multiple reserves for that bib can have the itemnumber set
575 ## the sub is only used once in the codebase.
576 sub GetReserveStatus {
577 my ($itemnumber) = @_;
579 my $dbh = C4::Context->dbh;
581 my ($sth, $found, $priority);
583 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
584 $sth->execute($itemnumber);
585 ($found, $priority) = $sth->fetchrow_array;
589 return 'Waiting' if $found eq 'W' and $priority == 0;
590 return 'Finished' if $found eq 'F';
593 return 'Reserved' if $priority > 0;
595 return ''; # empty string here will remove need for checking undef, or less log lines
600 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
601 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
602 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
604 Find a book in the reserves.
606 C<$itemnumber> is the book's item number.
607 C<$lookahead> is the number of days to look in advance for future reserves.
609 As I understand it, C<&CheckReserves> looks for the given item in the
610 reserves. If it is found, that's a match, and C<$status> is set to
613 Otherwise, it finds the most important item in the reserves with the
614 same biblio number as this book (I'm not clear on this) and returns it
615 with C<$status> set to C<Reserved>.
617 C<&CheckReserves> returns a two-element list:
619 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
621 C<$reserve> is the reserve item that matched. It is a
622 reference-to-hash whose keys are mostly the fields of the reserves
623 table in the Koha database.
628 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
629 my $dbh = C4::Context->dbh;
632 if (C4::Context->preference('item-level_itypes')){
634 SELECT items.biblionumber,
635 items.biblioitemnumber,
636 itemtypes.notforloan,
637 items.notforloan AS itemnotforloan,
643 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
644 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
649 SELECT items.biblionumber,
650 items.biblioitemnumber,
651 itemtypes.notforloan,
652 items.notforloan AS itemnotforloan,
658 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
659 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
664 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
665 $sth->execute($item);
668 $sth = $dbh->prepare("$select WHERE barcode = ?");
669 $sth->execute($barcode);
671 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
672 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
674 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
676 return unless $itemnumber; # bail if we got nothing.
678 # if item is not for loan it cannot be reserved either.....
679 # except where items.notforloan < 0 : This indicates the item is holdable.
680 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
682 # Find this item in the reserves
683 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
685 # $priority and $highest are used to find the most important item
686 # in the list returned by &_Findgroupreserve. (The lower $priority,
687 # the more important the item.)
688 # $highest is the most important item we've seen so far.
690 if (scalar @reserves) {
691 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
692 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
693 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
695 my $priority = 10000000;
696 foreach my $res (@reserves) {
697 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
698 if ($res->{'found'} eq 'W') {
699 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
701 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
706 my $local_hold_match;
708 if ($LocalHoldsPriority) {
709 $patron = Koha::Patrons->find( $res->{borrowernumber} );
710 $iteminfo = C4::Items::GetItem($itemnumber);
712 my $local_holds_priority_item_branchcode =
713 $iteminfo->{$LocalHoldsPriorityItemControl};
714 my $local_holds_priority_patron_branchcode =
715 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
717 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
718 ? $patron->branchcode
721 $local_holds_priority_item_branchcode eq
722 $local_holds_priority_patron_branchcode;
725 # See if this item is more important than what we've got so far
726 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
727 $iteminfo ||= C4::Items::GetItem($itemnumber);
728 next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
729 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
730 my $branch = GetReservesControlBranch( $iteminfo, $patron->unblessed );
731 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
732 next if ($branchitemrule->{'holdallowed'} == 0);
733 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
734 next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
735 $priority = $res->{'priority'};
737 last if $local_hold_match;
743 # If we get this far, then no exact match was found.
744 # We return the most important (i.e. next) reservation.
746 $highest->{'itemnumber'} = $item;
747 return ( "Reserved", $highest, \@reserves );
753 =head2 CancelExpiredReserves
755 CancelExpiredReserves();
757 Cancels all reserves with an expiration date from before today.
761 sub CancelExpiredReserves {
762 my $today = dt_from_string();
763 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
764 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
766 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
767 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
768 $params->{found} = undef unless $expireWaiting;
770 # FIXME To move to Koha::Holds->search_expired (?)
771 my $holds = Koha::Holds->search( $params );
773 while ( my $hold = $holds->next ) {
774 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
776 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
778 my $cancel_params = {};
779 if ( $hold->found eq 'W' ) {
780 $cancel_params->{charge_cancel_fee} = 1;
782 $hold->cancel( $cancel_params );
786 =head2 AutoUnsuspendReserves
788 AutoUnsuspendReserves();
790 Unsuspends all suspended reserves with a suspend_until date from before today.
794 sub AutoUnsuspendReserves {
795 my $today = dt_from_string();
797 my @holds = Koha::Holds->search( { suspend_until => { '<' => $today->ymd() } } );
799 map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
804 ModReserve({ rank => $rank,
805 reserve_id => $reserve_id,
806 branchcode => $branchcode
807 [, itemnumber => $itemnumber ]
808 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
811 Change a hold request's priority or cancel it.
813 C<$rank> specifies the effect of the change. If C<$rank>
814 is 'W' or 'n', nothing happens. This corresponds to leaving a
815 request alone when changing its priority in the holds queue
818 If C<$rank> is 'del', the hold request is cancelled.
820 If C<$rank> is an integer greater than zero, the priority of
821 the request is set to that value. Since priority != 0 means
822 that the item is not waiting on the hold shelf, setting the
823 priority to a non-zero value also sets the request's found
824 status and waiting date to NULL.
826 The optional C<$itemnumber> parameter is used only when
827 C<$rank> is a non-zero integer; if supplied, the itemnumber
828 of the hold request is set accordingly; if omitted, the itemnumber
831 B<FIXME:> Note that the forgoing can have the effect of causing
832 item-level hold requests to turn into title-level requests. This
833 will be fixed once reserves has separate columns for requested
834 itemnumber and supplying itemnumber.
841 my $rank = $params->{'rank'};
842 my $reserve_id = $params->{'reserve_id'};
843 my $branchcode = $params->{'branchcode'};
844 my $itemnumber = $params->{'itemnumber'};
845 my $suspend_until = $params->{'suspend_until'};
846 my $borrowernumber = $params->{'borrowernumber'};
847 my $biblionumber = $params->{'biblionumber'};
849 return if $rank eq "W";
850 return if $rank eq "n";
852 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
855 unless ( $reserve_id ) {
856 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
857 return unless $holds->count; # FIXME Should raise an exception
858 $hold = $holds->next;
859 $reserve_id = $hold->reserve_id;
862 $hold ||= Koha::Holds->find($reserve_id);
864 if ( $rank eq "del" ) {
867 elsif ($rank =~ /^\d+/ and $rank > 0) {
868 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
869 if C4::Context->preference('HoldsLog');
874 branchcode => $branchcode,
875 itemnumber => $itemnumber,
881 if ( defined( $suspend_until ) ) {
882 if ( $suspend_until ) {
883 $suspend_until = eval { dt_from_string( $suspend_until ) };
884 $hold->suspend_hold( $suspend_until );
886 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
887 # If the hold is not suspended, this does nothing.
888 $hold->set( { suspend_until => undef } )->store();
892 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
896 =head2 ModReserveFill
898 &ModReserveFill($reserve);
900 Fill a reserve. If I understand this correctly, this means that the
901 reserved book has been found and given to the patron who reserved it.
903 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
904 whose keys are fields from the reserves table in the Koha database.
910 my $reserve_id = $res->{'reserve_id'};
912 my $hold = Koha::Holds->find($reserve_id);
914 # get the priority on this record....
915 my $priority = $hold->priority;
917 # update the hold statuses, no need to store it though, we will be deleting it anyway
925 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
926 Koha::Old::Hold->new( $hold->unblessed() )->store();
930 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
931 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
932 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
935 # now fix the priority on the others (if the priority wasn't
936 # already sorted!)....
937 unless ( $priority == 0 ) {
938 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
942 =head2 ModReserveStatus
944 &ModReserveStatus($itemnumber, $newstatus);
946 Update the reserve status for the active (priority=0) reserve.
948 $itemnumber is the itemnumber the reserve is on
950 $newstatus is the new status.
954 sub ModReserveStatus {
956 #first : check if we have a reservation for this item .
957 my ($itemnumber, $newstatus) = @_;
958 my $dbh = C4::Context->dbh;
960 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
961 my $sth_set = $dbh->prepare($query);
962 $sth_set->execute( $newstatus, $itemnumber );
964 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
965 CartToShelf( $itemnumber );
969 =head2 ModReserveAffect
971 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
973 This function affect an item and a status for a given reserve, either fetched directly
974 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
975 is given, only first reserve returned is affected, which is ok for anything but
978 if $transferToDo is not set, then the status is set to "Waiting" as well.
979 otherwise, a transfer is on the way, and the end of the transfer will
980 take care of the waiting status
984 sub ModReserveAffect {
985 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
986 my $dbh = C4::Context->dbh;
988 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
989 # attached to $itemnumber
990 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
991 $sth->execute($itemnumber);
992 my ($biblionumber) = $sth->fetchrow;
994 # get request - need to find out if item is already
995 # waiting in order to not send duplicate hold filled notifications
998 # Find hold by id if we have it
999 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1000 # Find item level hold for this item if there is one
1001 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1002 # Find record level hold if there is no item level hold
1003 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1005 return unless $hold;
1007 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1009 $hold->itemnumber($itemnumber);
1010 $hold->set_waiting($transferToDo);
1012 _koha_notify_reserve( $hold->reserve_id )
1013 if ( !$transferToDo && !$already_on_shelf );
1015 _FixPriority( { biblionumber => $biblionumber } );
1017 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1018 CartToShelf($itemnumber);
1024 =head2 ModReserveCancelAll
1026 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1028 function to cancel reserv,check other reserves, and transfer document if it's necessary
1032 sub ModReserveCancelAll {
1035 my ( $itemnumber, $borrowernumber ) = @_;
1037 #step 1 : cancel the reservation
1038 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1039 return unless $holds->count;
1040 $holds->next->cancel;
1042 #step 2 launch the subroutine of the others reserves
1043 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1045 return ( $messages, $nextreservinfo );
1048 =head2 ModReserveMinusPriority
1050 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1052 Reduce the values of queued list
1056 sub ModReserveMinusPriority {
1057 my ( $itemnumber, $reserve_id ) = @_;
1059 #first step update the value of the first person on reserv
1060 my $dbh = C4::Context->dbh;
1063 SET priority = 0 , itemnumber = ?
1064 WHERE reserve_id = ?
1066 my $sth_upd = $dbh->prepare($query);
1067 $sth_upd->execute( $itemnumber, $reserve_id );
1068 # second step update all others reserves
1069 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1072 =head2 IsAvailableForItemLevelRequest
1074 my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1076 Checks whether a given item record is available for an
1077 item-level hold request. An item is available if
1079 * it is not lost AND
1080 * it is not damaged AND
1081 * it is not withdrawn AND
1082 * a waiting or in transit reserve is placed on
1083 * does not have a not for loan value > 0
1085 Need to check the issuingrules onshelfholds column,
1086 if this is set items on the shelf can be placed on hold
1088 Note that IsAvailableForItemLevelRequest() does not
1089 check if the staff operator is authorized to place
1090 a request on the item - in particular,
1091 this routine does not check IndependentBranches
1092 and canreservefromotherbranches.
1096 sub IsAvailableForItemLevelRequest {
1098 my $borrower = shift;
1100 my $dbh = C4::Context->dbh;
1101 # must check the notforloan setting of the itemtype
1102 # FIXME - a lot of places in the code do this
1103 # or something similar - need to be
1105 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
1106 my $item_object = Koha::Items->find( $item->{itemnumber } );
1107 my $itemtype = $item_object->effective_itemtype;
1108 my $notforloan_per_itemtype
1109 = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1113 $notforloan_per_itemtype ||
1114 $item->{itemlost} ||
1115 $item->{notforloan} > 0 ||
1116 $item->{withdrawn} ||
1117 ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1119 my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item_object, patron => $patron } );
1121 if ( $on_shelf_holds == 1 ) {
1123 } elsif ( $on_shelf_holds == 2 ) {
1125 Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1127 my $any_available = 0;
1129 foreach my $i (@items) {
1131 my $circ_control_branch = C4::Circulation::_GetCircControlBranch( $i->unblessed(), $borrower );
1132 my $branchitemrule = C4::Circulation::GetBranchItemRule( $circ_control_branch, $i->itype );
1136 || $i->notforloan > 0
1139 || IsItemOnHoldAndFound( $i->id )
1141 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1142 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1143 || $branchitemrule->{holdallowed} == 1 && $borrower->{branchcode} ne $i->homebranch;
1146 return $any_available ? 0 : 1;
1147 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1148 return $item->{onloan} || IsItemOnHoldAndFound( $item->{itemnumber} );
1156 if (C4::Context->preference('item-level_itypes')) {
1157 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1158 # When GetItem is fixed, we can remove this
1159 $itype = $item->{itype};
1162 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1163 # So if we already have a biblioitems join when calling this function,
1164 # we don't need to access the database again
1165 $itype = $item->{itemtype};
1168 my $dbh = C4::Context->dbh;
1169 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1170 my $sth = $dbh->prepare($query);
1171 $sth->execute($item->{biblioitemnumber});
1172 if (my $data = $sth->fetchrow_hashref()){
1173 $itype = $data->{itemtype};
1179 =head2 AlterPriority
1181 AlterPriority( $where, $reserve_id );
1183 This function changes a reserve's priority up, down, to the top, or to the bottom.
1184 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1189 my ( $where, $reserve_id ) = @_;
1191 my $hold = Koha::Holds->find( $reserve_id );
1192 return unless $hold;
1194 if ( $hold->cancellationdate ) {
1195 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1199 if ( $where eq 'up' || $where eq 'down' ) {
1201 my $priority = $hold->priority;
1202 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1203 _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1205 } elsif ( $where eq 'top' ) {
1207 _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1209 } elsif ( $where eq 'bottom' ) {
1211 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1214 # FIXME Should return the new priority
1217 =head2 ToggleLowestPriority
1219 ToggleLowestPriority( $borrowernumber, $biblionumber );
1221 This function sets the lowestPriority field to true if is false, and false if it is true.
1225 sub ToggleLowestPriority {
1226 my ( $reserve_id ) = @_;
1228 my $dbh = C4::Context->dbh;
1230 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1231 $sth->execute( $reserve_id );
1233 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1236 =head2 ToggleSuspend
1238 ToggleSuspend( $reserve_id );
1240 This function sets the suspend field to true if is false, and false if it is true.
1241 If the reserve is currently suspended with a suspend_until date, that date will
1242 be cleared when it is unsuspended.
1247 my ( $reserve_id, $suspend_until ) = @_;
1249 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1251 my $hold = Koha::Holds->find( $reserve_id );
1253 if ( $hold->is_suspended ) {
1256 $hold->suspend_hold( $suspend_until );
1263 borrowernumber => $borrowernumber,
1264 [ biblionumber => $biblionumber, ]
1265 [ suspend_until => $suspend_until, ]
1266 [ suspend => $suspend ]
1269 This function accepts a set of hash keys as its parameters.
1270 It requires either borrowernumber or biblionumber, or both.
1272 suspend_until is wholly optional.
1279 my $borrowernumber = $params{'borrowernumber'} || undef;
1280 my $biblionumber = $params{'biblionumber'} || undef;
1281 my $suspend_until = $params{'suspend_until'} || undef;
1282 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1284 $suspend_until = eval { dt_from_string($suspend_until) }
1285 if ( defined($suspend_until) );
1287 return unless ( $borrowernumber || $biblionumber );
1290 $params->{found} = undef;
1291 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1292 $params->{biblionumber} = $biblionumber if $biblionumber;
1294 my @holds = Koha::Holds->search($params);
1297 map { $_->suspend_hold($suspend_until) } @holds;
1300 map { $_->resume() } @holds;
1308 reserve_id => $reserve_id,
1310 [ignoreSetLowestRank => $ignoreSetLowestRank]
1315 _FixPriority({ biblionumber => $biblionumber});
1317 This routine adjusts the priority of a hold request and holds
1320 In the first form, where a reserve_id is passed, the priority of the
1321 hold is set to supplied rank, and other holds for that bib are adjusted
1322 accordingly. If the rank is "del", the hold is cancelled. If no rank
1323 is supplied, all of the holds on that bib have their priority adjusted
1324 as if the second form had been used.
1326 In the second form, where a biblionumber is passed, the holds on that
1327 bib (that are not captured) are sorted in order of increasing priority,
1328 then have reserves.priority set so that the first non-captured hold
1329 has its priority set to 1, the second non-captured hold has its priority
1330 set to 2, and so forth.
1332 In both cases, holds that have the lowestPriority flag on are have their
1333 priority adjusted to ensure that they remain at the end of the line.
1335 Note that the ignoreSetLowestRank parameter is meant to be used only
1336 when _FixPriority calls itself.
1341 my ( $params ) = @_;
1342 my $reserve_id = $params->{reserve_id};
1343 my $rank = $params->{rank} // '';
1344 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1345 my $biblionumber = $params->{biblionumber};
1347 my $dbh = C4::Context->dbh;
1350 if ( $reserve_id ) {
1351 $hold = Koha::Holds->find( $reserve_id );
1352 return unless $hold;
1355 unless ( $biblionumber ) { # FIXME This is a very weird API
1356 $biblionumber = $hold->biblionumber;
1359 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1362 elsif ( $rank eq "W" || $rank eq "0" ) {
1364 # make sure priority for waiting or in-transit items is 0
1368 WHERE reserve_id = ?
1369 AND found IN ('W', 'T')
1371 my $sth = $dbh->prepare($query);
1372 $sth->execute( $reserve_id );
1378 SELECT reserve_id, borrowernumber, reservedate
1380 WHERE biblionumber = ?
1381 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1382 ORDER BY priority ASC
1384 my $sth = $dbh->prepare($query);
1385 $sth->execute( $biblionumber );
1386 while ( my $line = $sth->fetchrow_hashref ) {
1387 push( @priority, $line );
1390 # To find the matching index
1392 my $key = -1; # to allow for 0 to be a valid result
1393 for ( $i = 0 ; $i < @priority ; $i++ ) {
1394 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1395 $key = $i; # save the index
1400 # if index exists in array then move it to new position
1401 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1402 my $new_rank = $rank -
1403 1; # $new_rank is what you want the new index to be in the array
1404 my $moving_item = splice( @priority, $key, 1 );
1405 splice( @priority, $new_rank, 0, $moving_item );
1408 # now fix the priority on those that are left....
1412 WHERE reserve_id = ?
1414 $sth = $dbh->prepare($query);
1415 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1418 $priority[$j]->{'reserve_id'}
1422 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1425 unless ( $ignoreSetLowestRank ) {
1426 while ( my $res = $sth->fetchrow_hashref() ) {
1428 reserve_id => $res->{'reserve_id'},
1430 ignoreSetLowestRank => 1
1436 =head2 _Findgroupreserve
1438 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1440 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1441 first match found. If neither, then we look for non-holds-queue based holds.
1442 Lookahead is the number of days to look in advance.
1444 C<&_Findgroupreserve> returns :
1445 C<@results> is an array of references-to-hash whose keys are mostly
1446 fields from the reserves table of the Koha database, plus
1447 C<biblioitemnumber>.
1451 sub _Findgroupreserve {
1452 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1453 my $dbh = C4::Context->dbh;
1455 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1456 # check for exact targeted match
1457 my $item_level_target_query = qq{
1458 SELECT reserves.biblionumber AS biblionumber,
1459 reserves.borrowernumber AS borrowernumber,
1460 reserves.reservedate AS reservedate,
1461 reserves.branchcode AS branchcode,
1462 reserves.cancellationdate AS cancellationdate,
1463 reserves.found AS found,
1464 reserves.reservenotes AS reservenotes,
1465 reserves.priority AS priority,
1466 reserves.timestamp AS timestamp,
1467 biblioitems.biblioitemnumber AS biblioitemnumber,
1468 reserves.itemnumber AS itemnumber,
1469 reserves.reserve_id AS reserve_id,
1470 reserves.itemtype AS itemtype
1472 JOIN biblioitems USING (biblionumber)
1473 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1476 AND item_level_request = 1
1478 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1482 my $sth = $dbh->prepare($item_level_target_query);
1483 $sth->execute($itemnumber, $lookahead||0);
1485 if ( my $data = $sth->fetchrow_hashref ) {
1486 push( @results, $data )
1487 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1489 return @results if @results;
1491 # check for title-level targeted match
1492 my $title_level_target_query = qq{
1493 SELECT reserves.biblionumber AS biblionumber,
1494 reserves.borrowernumber AS borrowernumber,
1495 reserves.reservedate AS reservedate,
1496 reserves.branchcode AS branchcode,
1497 reserves.cancellationdate AS cancellationdate,
1498 reserves.found AS found,
1499 reserves.reservenotes AS reservenotes,
1500 reserves.priority AS priority,
1501 reserves.timestamp AS timestamp,
1502 biblioitems.biblioitemnumber AS biblioitemnumber,
1503 reserves.itemnumber AS itemnumber,
1504 reserves.reserve_id AS reserve_id,
1505 reserves.itemtype AS itemtype
1507 JOIN biblioitems USING (biblionumber)
1508 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1511 AND item_level_request = 0
1512 AND hold_fill_targets.itemnumber = ?
1513 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1517 $sth = $dbh->prepare($title_level_target_query);
1518 $sth->execute($itemnumber, $lookahead||0);
1520 if ( my $data = $sth->fetchrow_hashref ) {
1521 push( @results, $data )
1522 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1524 return @results if @results;
1527 SELECT reserves.biblionumber AS biblionumber,
1528 reserves.borrowernumber AS borrowernumber,
1529 reserves.reservedate AS reservedate,
1530 reserves.waitingdate AS waitingdate,
1531 reserves.branchcode AS branchcode,
1532 reserves.cancellationdate AS cancellationdate,
1533 reserves.found AS found,
1534 reserves.reservenotes AS reservenotes,
1535 reserves.priority AS priority,
1536 reserves.timestamp AS timestamp,
1537 reserves.itemnumber AS itemnumber,
1538 reserves.reserve_id AS reserve_id,
1539 reserves.itemtype AS itemtype
1541 WHERE reserves.biblionumber = ?
1542 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1543 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1547 $sth = $dbh->prepare($query);
1548 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1550 while ( my $data = $sth->fetchrow_hashref ) {
1551 push( @results, $data )
1552 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1557 =head2 _koha_notify_reserve
1559 _koha_notify_reserve( $hold->reserve_id );
1561 Sends a notification to the patron that their hold has been filled (through
1562 ModReserveAffect, _not_ ModReserveFill)
1564 The letter code for this notice may be found using the following query:
1566 select distinct letter_code
1567 from message_transports
1568 inner join message_attributes using (message_attribute_id)
1569 where message_name = 'Hold_Filled'
1571 This will probably sipmly be 'HOLD', but because it is defined in the database,
1572 it is subject to addition or change.
1574 The following tables are availalbe witin the notice:
1585 sub _koha_notify_reserve {
1586 my $reserve_id = shift;
1587 my $hold = Koha::Holds->find($reserve_id);
1588 my $borrowernumber = $hold->borrowernumber;
1590 my $patron = Koha::Patrons->find( $borrowernumber );
1592 # Try to get the borrower's email address
1593 my $to_address = $patron->notice_email_address;
1595 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1596 borrowernumber => $borrowernumber,
1597 message_name => 'Hold_Filled'
1600 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1602 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1604 my %letter_params = (
1605 module => 'reserves',
1606 branchcode => $hold->branchcode,
1607 lang => $patron->lang,
1609 'branches' => $library,
1610 'borrowers' => $patron->unblessed,
1611 'biblio' => $hold->biblionumber,
1612 'biblioitems' => $hold->biblionumber,
1613 'reserves' => $hold->unblessed,
1614 'items' => $hold->itemnumber,
1618 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.
1619 my $send_notification = sub {
1620 my ( $mtt, $letter_code ) = (@_);
1621 return unless defined $letter_code;
1622 $letter_params{letter_code} = $letter_code;
1623 $letter_params{message_transport_type} = $mtt;
1624 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1626 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1630 C4::Letters::EnqueueLetter( {
1632 borrowernumber => $borrowernumber,
1633 from_address => $admin_email_address,
1634 message_transport_type => $mtt,
1638 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1640 ( $mtt eq 'email' and not $to_address ) # No email address
1641 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1642 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1645 &$send_notification($mtt, $letter_code);
1646 $notification_sent++;
1648 #Making sure that a print notification is sent if no other transport types can be utilized.
1649 if (! $notification_sent) {
1650 &$send_notification('print', 'HOLD');
1655 =head2 _ShiftPriorityByDateAndPriority
1657 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1659 This increments the priority of all reserves after the one
1660 with either the lowest date after C<$reservedate>
1661 or the lowest priority after C<$priority>.
1663 It effectively makes room for a new reserve to be inserted with a certain
1664 priority, which is returned.
1666 This is most useful when the reservedate can be set by the user. It allows
1667 the new reserve to be placed before other reserves that have a later
1668 reservedate. Since priority also is set by the form in reserves/request.pl
1669 the sub accounts for that too.
1673 sub _ShiftPriorityByDateAndPriority {
1674 my ( $biblio, $resdate, $new_priority ) = @_;
1676 my $dbh = C4::Context->dbh;
1677 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1678 my $sth = $dbh->prepare( $query );
1679 $sth->execute( $biblio, $resdate, $new_priority );
1680 my $min_priority = $sth->fetchrow;
1681 # if no such matches are found, $new_priority remains as original value
1682 $new_priority = $min_priority if ( $min_priority );
1684 # Shift the priority up by one; works in conjunction with the next SQL statement
1685 $query = "UPDATE reserves
1686 SET priority = priority+1
1687 WHERE biblionumber = ?
1688 AND borrowernumber = ?
1691 my $sth_update = $dbh->prepare( $query );
1693 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1694 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1695 $sth = $dbh->prepare( $query );
1696 $sth->execute( $new_priority, $biblio );
1697 while ( my $row = $sth->fetchrow_hashref ) {
1698 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1701 return $new_priority; # so the caller knows what priority they wind up receiving
1706 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1708 Use when checking out an item to handle reserves
1709 If $cancelreserve boolean is set to true, it will remove existing reserve
1714 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1716 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1717 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
1720 my $biblionumber = $res->{biblionumber};
1722 if ($res->{borrowernumber} == $borrowernumber) {
1723 ModReserveFill($res);
1727 # The item is reserved by someone else.
1728 # Find this item in the reserves
1731 foreach (@$all_reserves) {
1732 $_->{'borrowernumber'} == $borrowernumber or next;
1733 $_->{'biblionumber'} == $biblionumber or next;
1740 # The item is reserved by the current patron
1741 ModReserveFill($borr_res);
1744 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1745 RevertWaitingStatus({ itemnumber => $itemnumber });
1747 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1748 my $hold = Koha::Holds->find( $res->{reserve_id} );
1756 MergeHolds($dbh,$to_biblio, $from_biblio);
1758 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1763 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1764 my $sth = $dbh->prepare(
1765 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1767 $sth->execute($from_biblio);
1768 if ( my $data = $sth->fetchrow_hashref() ) {
1770 # holds exist on old record, if not we don't need to do anything
1771 $sth = $dbh->prepare(
1772 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1773 $sth->execute( $to_biblio, $from_biblio );
1776 # don't reorder those already waiting
1778 $sth = $dbh->prepare(
1779 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1781 my $upd_sth = $dbh->prepare(
1782 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1783 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1785 $sth->execute( $to_biblio, 'W', 'T' );
1787 while ( my $reserve = $sth->fetchrow_hashref() ) {
1789 $priority, $to_biblio,
1790 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1791 $reserve->{'itemnumber'}
1798 =head2 RevertWaitingStatus
1800 RevertWaitingStatus({ itemnumber => $itemnumber });
1802 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1804 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1805 item level hold, even if it was only a bibliolevel hold to
1806 begin with. This is because we can no longer know if a hold
1807 was item-level or bib-level after a hold has been set to
1812 sub RevertWaitingStatus {
1813 my ( $params ) = @_;
1814 my $itemnumber = $params->{'itemnumber'};
1816 return unless ( $itemnumber );
1818 my $dbh = C4::Context->dbh;
1820 ## Get the waiting reserve we want to revert
1822 SELECT * FROM reserves
1823 WHERE itemnumber = ?
1824 AND found IS NOT NULL
1826 my $sth = $dbh->prepare( $query );
1827 $sth->execute( $itemnumber );
1828 my $reserve = $sth->fetchrow_hashref();
1830 ## Increment the priority of all other non-waiting
1831 ## reserves for this bib record
1835 priority = priority + 1
1841 $sth = $dbh->prepare( $query );
1842 $sth->execute( $reserve->{'biblionumber'} );
1844 ## Fix up the currently waiting reserve
1854 $sth = $dbh->prepare( $query );
1855 $sth->execute( $reserve->{'reserve_id'} );
1856 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1861 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
1863 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1865 The letter code will be HOLD_SLIP, and the following tables are
1866 available within the slip:
1878 my ($branch, $borrowernumber, $biblionumber) = @_;
1880 # return unless ( C4::Context->boolean_preference('printreserveslips') );
1881 my $patron = Koha::Patrons->find( $borrowernumber );
1883 my $hold = Koha::Holds->search({biblionumber => $biblionumber, borrowernumber => $borrowernumber })->next;
1884 return unless $hold;
1885 my $reserve = $hold->unblessed;
1887 return C4::Letters::GetPreparedLetter (
1888 module => 'circulation',
1889 letter_code => 'HOLD_SLIP',
1890 branchcode => $branch,
1891 lang => $patron->lang,
1893 'reserves' => $reserve,
1894 'branches' => $reserve->{branchcode},
1895 'borrowers' => $reserve->{borrowernumber},
1896 'biblio' => $reserve->{biblionumber},
1897 'biblioitems' => $reserve->{biblionumber},
1898 'items' => $reserve->{itemnumber},
1903 =head2 GetReservesControlBranch
1905 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
1907 Return the branchcode to be used to determine which reserves
1908 policy applies to a transaction.
1910 C<$item> is a hashref for an item. Only 'homebranch' is used.
1912 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
1916 sub GetReservesControlBranch {
1917 my ( $item, $borrower ) = @_;
1919 my $reserves_control = C4::Context->preference('ReservesControlBranch');
1922 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
1923 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
1929 =head2 CalculatePriority
1931 my $p = CalculatePriority($biblionumber, $resdate);
1933 Calculate priority for a new reserve on biblionumber, placing it at
1934 the end of the line of all holds whose start date falls before
1935 the current system time and that are neither on the hold shelf
1938 The reserve date parameter is optional; if it is supplied, the
1939 priority is based on the set of holds whose start date falls before
1940 the parameter value.
1942 After calculation of this priority, it is recommended to call
1943 _ShiftPriorityByDateAndPriority. Note that this is currently done in
1948 sub CalculatePriority {
1949 my ( $biblionumber, $resdate ) = @_;
1952 SELECT COUNT(*) FROM reserves
1953 WHERE biblionumber = ?
1955 AND (found IS NULL OR found = '')
1957 #skip found==W or found==T (waiting or transit holds)
1959 $sql.= ' AND ( reservedate <= ? )';
1962 $sql.= ' AND ( reservedate < NOW() )';
1964 my $dbh = C4::Context->dbh();
1965 my @row = $dbh->selectrow_array(
1968 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
1971 return @row ? $row[0]+1 : 1;
1974 =head2 IsItemOnHoldAndFound
1976 my $bool = IsItemFoundHold( $itemnumber );
1978 Returns true if the item is currently on hold
1979 and that hold has a non-null found status ( W, T, etc. )
1983 sub IsItemOnHoldAndFound {
1984 my ($itemnumber) = @_;
1986 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
1988 my $found = $rs->count(
1990 itemnumber => $itemnumber,
1991 found => { '!=' => undef }
1998 =head2 GetMaxPatronHoldsForRecord
2000 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2002 For multiple holds on a given record for a given patron, the max
2003 number of record level holds that a patron can be placed is the highest
2004 value of the holds_per_record rule for each item if the record for that
2005 patron. This subroutine finds and returns the highest holds_per_record
2006 rule value for a given patron id and record id.
2010 sub GetMaxPatronHoldsForRecord {
2011 my ( $borrowernumber, $biblionumber ) = @_;
2013 my $patron = Koha::Patrons->find($borrowernumber);
2014 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2016 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2018 my $categorycode = $patron->categorycode;
2020 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2023 foreach my $item (@items) {
2024 my $itemtype = $item->effective_itemtype();
2026 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2028 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2029 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2030 $max = $holds_per_record if $holds_per_record > $max;
2038 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2040 Returns the matching hold related issuingrule fields for a given
2041 patron category, itemtype, and library.
2046 my ( $categorycode, $itemtype, $branchcode ) = @_;
2048 my $dbh = C4::Context->dbh;
2050 my $sth = $dbh->prepare(
2052 SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record
2054 WHERE (categorycode in (?,'*') )
2055 AND (itemtype IN (?,'*'))
2056 AND (branchcode IN (?,'*'))
2057 ORDER BY categorycode DESC,
2063 $sth->execute( $categorycode, $itemtype, $branchcode );
2065 return $sth->fetchrow_hashref();
2070 Koha Development Team <http://koha-community.org/>