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>.
33 use C4::Members::Messaging;
35 use Koha::Account::Lines;
38 use Koha::CirculationRules;
51 use List::MoreUtils qw( firstidx any );
53 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
57 C4::Reserves - Koha functions for dealing with reservation.
65 This modules provides somes functions to deal with reservations.
67 Reserves are stored in reserves table.
68 The following columns contains important values :
69 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
70 =0 : then the reserve is being dealed
71 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
72 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
73 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
74 F(inished) : the reserve has been completed, and is done
75 - itemnumber : empty : the reserve is still unaffected to an item
76 filled: the reserve is attached to an item
77 The complete workflow is :
78 ==== 1st use case ====
79 patron request a document, 1st available : P >0, F=NULL, I=NULL
80 a library having it run "transfertodo", and clic on the list
81 if there is no transfer to do, the reserve waiting
82 patron can pick it up P =0, F=W, I=filled
83 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
84 The pickup library receive the book, it check in P =0, F=W, I=filled
85 The patron borrow the book P =0, F=F, I=filled
87 ==== 2nd use case ====
88 patron requests a document, a given item,
89 If pickup is holding branch P =0, F=W, I=filled
90 If transfer needed, write in branchtransfer P =0, F=T, I=filled
91 The pickup library receive the book, it checks it in P =0, F=W, I=filled
92 The patron borrow the book P =0, F=F, I=filled
113 &ModReserveMinusPriority
119 &CanReserveBeCanceledFromOpac
120 &CancelExpiredReserves
122 &AutoUnsuspendReserves
124 &IsAvailableForItemLevelRequest
127 &ToggleLowestPriority
133 &GetReservesControlBranch
137 GetMaxPatronHoldsForRecord
139 @EXPORT_OK = qw( MergeHolds );
144 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
146 Adds reserve and generates HOLDPLACED message.
148 The following tables are available witin the HOLDPLACED message:
161 $branch, $borrowernumber, $biblionumber, $bibitems,
162 $priority, $resdate, $expdate, $notes,
163 $title, $checkitem, $found, $itemtype
166 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
167 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
169 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
171 # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
172 # of the document, we force the value $priority and $found .
173 if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
174 my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
177 # If item is already checked out, it cannot be set waiting
180 # The item can't be waiting if it needs a transfer
181 && $item->holdingbranch eq $branch
183 # Similarly, if in transit it can't be waiting
184 && !$item->get_transfer
186 # If we can't hold damaged items, and it is damaged, it can't be waiting
187 && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
189 # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
190 && !$item->current_holds->count )
197 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
199 # Make room in reserves for this before those of a later reserve date
200 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
205 # If the reserv had the waiting status, we had the value of the resdate
206 if ( $found && $found eq 'W' ) {
207 $waitingdate = $resdate;
210 # Don't add itemtype limit if specific item is selected
211 $itemtype = undef if $checkitem;
213 # updates take place here
214 my $hold = Koha::Hold->new(
216 borrowernumber => $borrowernumber,
217 biblionumber => $biblionumber,
218 reservedate => $resdate,
219 branchcode => $branch,
220 priority => $priority,
221 reservenotes => $notes,
222 itemnumber => $checkitem,
224 waitingdate => $waitingdate,
225 expirationdate => $expdate,
226 itemtype => $itemtype,
227 item_level_hold => $checkitem ? 1 : 0,
230 $hold->set_waiting() if $found && $found eq 'W';
232 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
233 if C4::Context->preference('HoldsLog');
235 my $reserve_id = $hold->id();
237 # add a reserve fee if needed
238 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
239 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
240 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
243 _FixPriority({ biblionumber => $biblionumber});
245 # Send e-mail to librarian if syspref is active
246 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
247 my $patron = Koha::Patrons->find( $borrowernumber );
248 my $library = $patron->library;
249 if ( my $letter = C4::Letters::GetPreparedLetter (
250 module => 'reserves',
251 letter_code => 'HOLDPLACED',
252 branchcode => $branch,
253 lang => $patron->lang,
255 'branches' => $library->unblessed,
256 'borrowers' => $patron->unblessed,
257 'biblio' => $biblionumber,
258 'biblioitems' => $biblionumber,
259 'items' => $checkitem,
260 'reserves' => $hold->unblessed,
264 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
266 C4::Letters::EnqueueLetter(
268 borrowernumber => $borrowernumber,
269 message_transport_type => 'email',
270 from_address => $admin_email_address,
271 to_address => $admin_email_address,
280 =head2 CanBookBeReserved
282 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
283 if ($canReserve eq 'OK') { #We can reserve this Item! }
285 See CanItemBeReserved() for possible return values.
289 sub CanBookBeReserved{
290 my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
292 my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
293 #get items linked via host records
294 my @hostitems = get_hostitemnumbers_of($biblionumber);
296 push (@itemnumbers, @hostitems);
299 my $canReserve = { status => '' };
300 foreach my $itemnumber (@itemnumbers) {
301 $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
302 return { status => 'OK' } if $canReserve->{status} eq 'OK';
307 =head2 CanItemBeReserved
309 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
310 if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
312 @RETURNS { status => OK }, if the Item can be reserved.
313 { status => ageRestricted }, if the Item is age restricted for this borrower.
314 { status => damaged }, if the Item is damaged.
315 { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
316 { status => branchNotInHoldGroup }, if borrower home library is not in hold group, and holds are only allowed from hold groups.
317 { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
318 { status => notReservable }, if holds on this item are not allowed
319 { status => libraryNotFound }, if given branchcode is not an existing library
320 { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location
321 { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
322 { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
326 sub CanItemBeReserved {
327 my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
329 my $dbh = C4::Context->dbh;
330 my $ruleitemtype; # itemtype of the matching issuing rule
331 my $allowedreserves = 0; # Total number of holds allowed across all records
332 my $holds_per_record = 1; # Total number of holds allowed for this one given record
333 my $holds_per_day; # Default to unlimited
335 # we retrieve borrowers and items informations #
336 # item->{itype} will come for biblioitems if necessery
337 my $item = Koha::Items->find($itemnumber);
338 my $biblio = $item->biblio;
339 my $patron = Koha::Patrons->find( $borrowernumber );
340 my $borrower = $patron->unblessed;
342 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
343 return { status =>'damaged' }
345 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
347 # Check for the age restriction
348 my ( $ageRestriction, $daysToAgeRestriction ) =
349 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
350 return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
352 # Check that the patron doesn't have an item level hold on this item already
353 return { status =>'itemAlreadyOnHold' }
354 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
356 my $controlbranch = C4::Context->preference('ReservesControlBranch');
359 SELECT count(*) AS count
361 LEFT JOIN items USING (itemnumber)
362 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
363 LEFT JOIN borrowers USING (borrowernumber)
364 WHERE borrowernumber = ?
368 my $branchfield = "reserves.branchcode";
370 if ( $controlbranch eq "ItemHomeLibrary" ) {
371 $branchfield = "items.homebranch";
372 $branchcode = $item->homebranch;
374 elsif ( $controlbranch eq "PatronLibrary" ) {
375 $branchfield = "borrowers.branchcode";
376 $branchcode = $borrower->{branchcode};
380 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->effective_itemtype, $branchcode ) ) {
381 $ruleitemtype = $rights->{itemtype};
382 $allowedreserves = $rights->{reservesallowed};
383 $holds_per_record = $rights->{holds_per_record};
384 $holds_per_day = $rights->{holds_per_day};
387 $ruleitemtype = undef;
390 my $holds = Koha::Holds->search(
392 borrowernumber => $borrowernumber,
393 biblionumber => $item->biblionumber,
396 if ( $holds->count() >= $holds_per_record ) {
397 return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
400 my $today_holds = Koha::Holds->search({
401 borrowernumber => $borrowernumber,
402 reservedate => dt_from_string->date
405 if ( defined $holds_per_day &&
406 ( ( $holds_per_day > 0 && $today_holds->count() >= $holds_per_day )
407 or ( $holds_per_day == 0 ) )
409 return { status => 'tooManyReservesToday', limit => $holds_per_day };
414 $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
416 # If using item-level itypes, fall back to the record
417 # level itemtype if the hold has no associated item
419 C4::Context->preference('item-level_itypes')
420 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
421 : " AND biblioitems.itemtype = ?"
422 if defined $ruleitemtype;
424 my $sthcount = $dbh->prepare($querycount);
426 if ( defined $ruleitemtype ) {
427 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
430 $sthcount->execute( $borrowernumber, $branchcode );
433 my $reservecount = "0";
434 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
435 $reservecount = $rowcount->{count};
438 # we check if it's ok or not
439 if ( $reservecount >= $allowedreserves ) {
440 return { status => 'tooManyReserves', limit => $allowedreserves };
443 # Now we need to check hold limits by patron category
444 my $rule = Koha::CirculationRules->get_effective_rule(
446 categorycode => $borrower->{categorycode},
447 branchcode => $branchcode,
448 rule_name => 'max_holds',
451 if ( $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
452 my $total_holds_count = Koha::Holds->search(
454 borrowernumber => $borrower->{borrowernumber}
458 return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
461 my $reserves_control_branch =
462 GetReservesControlBranch( $item->unblessed(), $borrower );
464 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype ); # FIXME Should not be item->effective_itemtype?
466 if ( $branchitemrule->{holdallowed} == 0 ) {
467 return { status => 'notReservable' };
470 if ( $branchitemrule->{holdallowed} == 1
471 && $borrower->{branchcode} ne $item->homebranch )
473 return { status => 'cannotReserveFromOtherBranches' };
476 my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
477 if ( $branchitemrule->{holdallowed} == 3) {
478 if($borrower->{branchcode} ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $borrower->{branchcode}} )) {
479 return { status => 'branchNotInHoldGroup' };
483 # If reservecount is ok, we check item branch if IndependentBranches is ON
484 # and canreservefromotherbranches is OFF
485 if ( C4::Context->preference('IndependentBranches')
486 and !C4::Context->preference('canreservefromotherbranches') )
488 if ( $item->homebranch ne $borrower->{branchcode} ) {
489 return { status => 'cannotReserveFromOtherBranches' };
493 if ($pickup_branchcode) {
494 my $destination = Koha::Libraries->find({
495 branchcode => $pickup_branchcode,
498 unless ($destination) {
499 return { status => 'libraryNotFound' };
501 unless ($destination->pickup_location) {
502 return { status => 'libraryNotPickupLocation' };
504 unless ($item->can_be_transferred({ to => $destination })) {
505 return { status => 'cannotBeTransferred' };
507 unless ($branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
508 return { status => 'pickupNotInHoldGroup' };
510 unless ($branchitemrule->{hold_fulfillment_policy} ne 'patrongroup' || Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
511 return { status => 'pickupNotInHoldGroup' };
515 return { status => 'OK' };
518 =head2 CanReserveBeCanceledFromOpac
520 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
522 returns 1 if reserve can be cancelled by user from OPAC.
523 First check if reserve belongs to user, next checks if reserve is not in
524 transfer or waiting status
528 sub CanReserveBeCanceledFromOpac {
529 my ($reserve_id, $borrowernumber) = @_;
531 return unless $reserve_id and $borrowernumber;
532 my $reserve = Koha::Holds->find($reserve_id);
534 return 0 unless $reserve->borrowernumber == $borrowernumber;
535 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
541 =head2 GetOtherReserves
543 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
545 Check queued list of this document and check if this document must be transferred
549 sub GetOtherReserves {
550 my ($itemnumber) = @_;
553 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
554 if ($checkreserves) {
555 my $item = Koha::Items->find($itemnumber);
556 if ( $item->holdingbranch ne $checkreserves->{'branchcode'} ) {
557 $messages->{'transfert'} = $checkreserves->{'branchcode'};
558 #minus priorities of others reservs
559 ModReserveMinusPriority(
561 $checkreserves->{'reserve_id'},
564 #launch the subroutine dotransfer
565 C4::Items::ModItemTransfer(
567 $item->holdingbranch,
568 $checkreserves->{'branchcode'}
573 #step 2b : case of a reservation on the same branch, set the waiting status
575 $messages->{'waiting'} = 1;
576 ModReserveMinusPriority(
578 $checkreserves->{'reserve_id'},
580 ModReserveStatus($itemnumber,'W');
583 $nextreservinfo = $checkreserves->{'borrowernumber'};
586 return ( $messages, $nextreservinfo );
589 =head2 ChargeReserveFee
591 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
593 Charge the fee for a reserve (if $fee > 0)
597 sub ChargeReserveFee {
598 my ( $borrowernumber, $fee, $title ) = @_;
599 return if !$fee || $fee == 0; # the last test is needed to include 0.00
600 Koha::Account->new( { patron_id => $borrowernumber } )->add_debit(
603 description => $title,
605 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
606 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
607 interface => C4::Context->interface,
608 invoice_type => undef,
617 $fee = GetReserveFee( $borrowernumber, $biblionumber );
619 Calculate the fee for a reserve (if applicable).
624 my ( $borrowernumber, $biblionumber ) = @_;
626 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
629 SELECT COUNT(*) FROM items
630 LEFT JOIN issues USING (itemnumber)
631 WHERE items.biblionumber=? AND issues.issue_id IS NULL
634 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
637 my $dbh = C4::Context->dbh;
638 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
639 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
640 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
641 # This is a reconstruction of the old code:
642 # Compare number of items with items issued, and optionally check holds
643 # If not all items are issued and there are no holds: charge no fee
644 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
645 my ( $notissued, $reserved );
646 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
649 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
650 ( $biblionumber, $borrowernumber ) );
651 $fee = 0 if $reserved == 0;
657 =head2 GetReserveStatus
659 $reservestatus = GetReserveStatus($itemnumber);
661 Takes an itemnumber and returns the status of the reserve placed on it.
662 If several reserves exist, the reserve with the lower priority is given.
666 ## FIXME: I don't think this does what it thinks it does.
667 ## It only ever checks the first reserve result, even though
668 ## multiple reserves for that bib can have the itemnumber set
669 ## the sub is only used once in the codebase.
670 sub GetReserveStatus {
671 my ($itemnumber) = @_;
673 my $dbh = C4::Context->dbh;
675 my ($sth, $found, $priority);
677 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
678 $sth->execute($itemnumber);
679 ($found, $priority) = $sth->fetchrow_array;
683 return 'Waiting' if $found eq 'W' and $priority == 0;
684 return 'Finished' if $found eq 'F';
687 return 'Reserved' if $priority > 0;
689 return ''; # empty string here will remove need for checking undef, or less log lines
694 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber);
695 ($status, $matched_reserve, $possible_reserves) = &CheckReserves(undef, $barcode);
696 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
698 Find a book in the reserves.
700 C<$itemnumber> is the book's item number.
701 C<$lookahead> is the number of days to look in advance for future reserves.
703 As I understand it, C<&CheckReserves> looks for the given item in the
704 reserves. If it is found, that's a match, and C<$status> is set to
707 Otherwise, it finds the most important item in the reserves with the
708 same biblio number as this book (I'm not clear on this) and returns it
709 with C<$status> set to C<Reserved>.
711 C<&CheckReserves> returns a two-element list:
713 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
715 C<$reserve> is the reserve item that matched. It is a
716 reference-to-hash whose keys are mostly the fields of the reserves
717 table in the Koha database.
722 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
723 my $dbh = C4::Context->dbh;
726 if (C4::Context->preference('item-level_itypes')){
728 SELECT items.biblionumber,
729 items.biblioitemnumber,
730 itemtypes.notforloan,
731 items.notforloan AS itemnotforloan,
737 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
738 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
743 SELECT items.biblionumber,
744 items.biblioitemnumber,
745 itemtypes.notforloan,
746 items.notforloan AS itemnotforloan,
752 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
753 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
758 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
759 $sth->execute($item);
762 $sth = $dbh->prepare("$select WHERE barcode = ?");
763 $sth->execute($barcode);
765 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
766 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
767 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
769 return unless $itemnumber; # bail if we got nothing.
770 # if item is not for loan it cannot be reserved either.....
771 # except where items.notforloan < 0 : This indicates the item is holdable.
772 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
774 # Find this item in the reserves
775 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
777 # $priority and $highest are used to find the most important item
778 # in the list returned by &_Findgroupreserve. (The lower $priority,
779 # the more important the item.)
780 # $highest is the most important item we've seen so far.
782 if (scalar @reserves) {
783 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
784 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
785 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
787 my $priority = 10000000;
788 foreach my $res (@reserves) {
789 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
790 if ($res->{'found'} eq 'W') {
791 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
793 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
798 my $local_hold_match;
800 if ($LocalHoldsPriority) {
801 $patron = Koha::Patrons->find( $res->{borrowernumber} );
802 $item = Koha::Items->find($itemnumber);
804 my $local_holds_priority_item_branchcode =
805 $item->$LocalHoldsPriorityItemControl;
806 my $local_holds_priority_patron_branchcode =
807 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
809 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
810 ? $patron->branchcode
813 $local_holds_priority_item_branchcode eq
814 $local_holds_priority_patron_branchcode;
817 # See if this item is more important than what we've got so far
818 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
819 $item ||= Koha::Items->find($itemnumber);
820 next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
821 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
822 my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
823 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
824 next if ($branchitemrule->{'holdallowed'} == 0);
825 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
826 my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
827 next if (($branchitemrule->{'holdallowed'} == 3) && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
828 my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
829 next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
830 next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
831 next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
832 next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
833 $priority = $res->{'priority'};
835 last if $local_hold_match;
841 # If we get this far, then no exact match was found.
842 # We return the most important (i.e. next) reservation.
844 $highest->{'itemnumber'} = $item;
845 return ( "Reserved", $highest, \@reserves );
851 =head2 CancelExpiredReserves
853 CancelExpiredReserves();
855 Cancels all reserves with an expiration date from before today.
859 sub CancelExpiredReserves {
860 my $today = dt_from_string();
861 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
862 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
864 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
865 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
866 $params->{found} = [ { '!=', 'W' }, undef ] unless $expireWaiting;
868 # FIXME To move to Koha::Holds->search_expired (?)
869 my $holds = Koha::Holds->search( $params );
871 while ( my $hold = $holds->next ) {
872 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
874 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
876 my $cancel_params = {};
877 if ( $hold->found eq 'W' ) {
878 $cancel_params->{charge_cancel_fee} = 1;
880 $hold->cancel( $cancel_params );
884 =head2 AutoUnsuspendReserves
886 AutoUnsuspendReserves();
888 Unsuspends all suspended reserves with a suspend_until date from before today.
892 sub AutoUnsuspendReserves {
893 my $today = dt_from_string();
895 my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
897 map { $_->resume() } @holds;
902 ModReserve({ rank => $rank,
903 reserve_id => $reserve_id,
904 branchcode => $branchcode
905 [, itemnumber => $itemnumber ]
906 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
909 Change a hold request's priority or cancel it.
911 C<$rank> specifies the effect of the change. If C<$rank>
912 is 'W' or 'n', nothing happens. This corresponds to leaving a
913 request alone when changing its priority in the holds queue
916 If C<$rank> is 'del', the hold request is cancelled.
918 If C<$rank> is an integer greater than zero, the priority of
919 the request is set to that value. Since priority != 0 means
920 that the item is not waiting on the hold shelf, setting the
921 priority to a non-zero value also sets the request's found
922 status and waiting date to NULL.
924 The optional C<$itemnumber> parameter is used only when
925 C<$rank> is a non-zero integer; if supplied, the itemnumber
926 of the hold request is set accordingly; if omitted, the itemnumber
929 B<FIXME:> Note that the forgoing can have the effect of causing
930 item-level hold requests to turn into title-level requests. This
931 will be fixed once reserves has separate columns for requested
932 itemnumber and supplying itemnumber.
939 my $rank = $params->{'rank'};
940 my $reserve_id = $params->{'reserve_id'};
941 my $branchcode = $params->{'branchcode'};
942 my $itemnumber = $params->{'itemnumber'};
943 my $suspend_until = $params->{'suspend_until'};
944 my $borrowernumber = $params->{'borrowernumber'};
945 my $biblionumber = $params->{'biblionumber'};
947 return if $rank eq "W";
948 return if $rank eq "n";
950 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
953 unless ( $reserve_id ) {
954 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
955 return unless $holds->count; # FIXME Should raise an exception
956 $hold = $holds->next;
957 $reserve_id = $hold->reserve_id;
960 $hold ||= Koha::Holds->find($reserve_id);
962 if ( $rank eq "del" ) {
965 elsif ($rank =~ /^\d+/ and $rank > 0) {
966 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
967 if C4::Context->preference('HoldsLog');
971 branchcode => $branchcode,
972 itemnumber => $itemnumber,
976 if (exists $params->{reservedate}) {
977 $properties->{reservedate} = $params->{reservedate} || undef;
979 if (exists $params->{expirationdate}) {
980 $properties->{expirationdate} = $params->{expirationdate} || undef;
983 $hold->set($properties)->store();
985 if ( defined( $suspend_until ) ) {
986 if ( $suspend_until ) {
987 $suspend_until = eval { dt_from_string( $suspend_until ) };
988 $hold->suspend_hold( $suspend_until );
990 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
991 # If the hold is not suspended, this does nothing.
992 $hold->set( { suspend_until => undef } )->store();
996 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1000 =head2 ModReserveFill
1002 &ModReserveFill($reserve);
1004 Fill a reserve. If I understand this correctly, this means that the
1005 reserved book has been found and given to the patron who reserved it.
1007 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1008 whose keys are fields from the reserves table in the Koha database.
1012 sub ModReserveFill {
1014 my $reserve_id = $res->{'reserve_id'};
1016 my $hold = Koha::Holds->find($reserve_id);
1017 # get the priority on this record....
1018 my $priority = $hold->priority;
1020 # update the hold statuses, no need to store it though, we will be deleting it anyway
1028 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
1029 Koha::Old::Hold->new( $hold->unblessed() )->store();
1033 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1034 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1035 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1038 # now fix the priority on the others (if the priority wasn't
1039 # already sorted!)....
1040 unless ( $priority == 0 ) {
1041 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1045 =head2 ModReserveStatus
1047 &ModReserveStatus($itemnumber, $newstatus);
1049 Update the reserve status for the active (priority=0) reserve.
1051 $itemnumber is the itemnumber the reserve is on
1053 $newstatus is the new status.
1057 sub ModReserveStatus {
1059 #first : check if we have a reservation for this item .
1060 my ($itemnumber, $newstatus) = @_;
1061 my $dbh = C4::Context->dbh;
1063 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1064 my $sth_set = $dbh->prepare($query);
1065 $sth_set->execute( $newstatus, $itemnumber );
1067 my $item = Koha::Items->find($itemnumber);
1068 if ( $item->location && $item->location eq 'CART'
1069 && ( !$item->permanent_location || $item->permanent_location ne 'CART' )
1071 CartToShelf( $itemnumber );
1075 =head2 ModReserveAffect
1077 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1079 This function affect an item and a status for a given reserve, either fetched directly
1080 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1081 is given, only first reserve returned is affected, which is ok for anything but
1084 if $transferToDo is not set, then the status is set to "Waiting" as well.
1085 otherwise, a transfer is on the way, and the end of the transfer will
1086 take care of the waiting status
1090 sub ModReserveAffect {
1091 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1092 my $dbh = C4::Context->dbh;
1094 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1095 # attached to $itemnumber
1096 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1097 $sth->execute($itemnumber);
1098 my ($biblionumber) = $sth->fetchrow;
1100 # get request - need to find out if item is already
1101 # waiting in order to not send duplicate hold filled notifications
1104 # Find hold by id if we have it
1105 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1106 # Find item level hold for this item if there is one
1107 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1108 # Find record level hold if there is no item level hold
1109 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1111 return unless $hold;
1113 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1115 $hold->itemnumber($itemnumber);
1116 $hold->set_waiting($transferToDo);
1118 _koha_notify_reserve( $hold->reserve_id )
1119 if ( !$transferToDo && !$already_on_shelf );
1121 _FixPriority( { biblionumber => $biblionumber } );
1122 my $item = Koha::Items->find($itemnumber);
1123 if ( $item->location && $item->location eq 'CART'
1124 && ( !$item->permanent_location || $item->permanent_location ne 'CART' ) ) {
1125 CartToShelf( $itemnumber );
1131 =head2 ModReserveCancelAll
1133 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1135 function to cancel reserv,check other reserves, and transfer document if it's necessary
1139 sub ModReserveCancelAll {
1142 my ( $itemnumber, $borrowernumber ) = @_;
1144 #step 1 : cancel the reservation
1145 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1146 return unless $holds->count;
1147 $holds->next->cancel;
1149 #step 2 launch the subroutine of the others reserves
1150 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1152 return ( $messages, $nextreservinfo );
1155 =head2 ModReserveMinusPriority
1157 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1159 Reduce the values of queued list
1163 sub ModReserveMinusPriority {
1164 my ( $itemnumber, $reserve_id ) = @_;
1166 #first step update the value of the first person on reserv
1167 my $dbh = C4::Context->dbh;
1170 SET priority = 0 , itemnumber = ?
1171 WHERE reserve_id = ?
1173 my $sth_upd = $dbh->prepare($query);
1174 $sth_upd->execute( $itemnumber, $reserve_id );
1175 # second step update all others reserves
1176 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1179 =head2 IsAvailableForItemLevelRequest
1181 my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1183 Checks whether a given item record is available for an
1184 item-level hold request. An item is available if
1186 * it is not lost AND
1187 * it is not damaged AND
1188 * it is not withdrawn AND
1189 * a waiting or in transit reserve is placed on
1190 * does not have a not for loan value > 0
1192 Need to check the issuingrules onshelfholds column,
1193 if this is set items on the shelf can be placed on hold
1195 Note that IsAvailableForItemLevelRequest() does not
1196 check if the staff operator is authorized to place
1197 a request on the item - in particular,
1198 this routine does not check IndependentBranches
1199 and canreservefromotherbranches.
1203 sub IsAvailableForItemLevelRequest {
1204 my ( $item, $patron, $pickup_branchcode ) = @_;
1206 my $dbh = C4::Context->dbh;
1207 # must check the notforloan setting of the itemtype
1208 # FIXME - a lot of places in the code do this
1209 # or something similar - need to be
1211 my $itemtype = $item->effective_itemtype;
1212 my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1215 $notforloan_per_itemtype ||
1217 $item->notforloan > 0 ||
1219 ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1221 my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1223 if ($pickup_branchcode) {
1224 my $destination = Koha::Libraries->find($pickup_branchcode);
1225 return 0 unless $destination;
1226 return 0 unless $destination->pickup_location;
1227 return 0 unless $item->can_be_transferred( { to => $destination } );
1228 my $reserves_control_branch =
1229 GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
1230 my $branchitemrule =
1231 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
1232 my $home_library = Koka::Libraries->find( {branchcode => $item->homebranch} );
1233 return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
1236 if ( $on_shelf_holds == 1 ) {
1238 } elsif ( $on_shelf_holds == 2 ) {
1240 Koha::Items->search( { biblionumber => $item->biblionumber } );
1242 my $any_available = 0;
1244 foreach my $i (@items) {
1245 my $reserves_control_branch = GetReservesControlBranch( $i->unblessed(), $patron->unblessed );
1246 my $branchitemrule = C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1247 my $item_library = Koha::Libraries->find( {branchcode => $i->homebranch} );
1252 || $i->notforloan > 0
1255 || IsItemOnHoldAndFound( $i->id )
1257 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1258 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1259 || $branchitemrule->{holdallowed} == 1 && $patron->branchcode ne $i->homebranch
1260 || $branchitemrule->{holdallowed} == 3 && !$item_library->validate_hold_sibling( {branchcode => $patron->branchcode} );
1263 return $any_available ? 0 : 1;
1264 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1265 return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1273 if (C4::Context->preference('item-level_itypes')) {
1274 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1275 # When GetItem is fixed, we can remove this
1276 $itype = $item->{itype};
1279 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1280 # So if we already have a biblioitems join when calling this function,
1281 # we don't need to access the database again
1282 $itype = $item->{itemtype};
1285 my $dbh = C4::Context->dbh;
1286 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1287 my $sth = $dbh->prepare($query);
1288 $sth->execute($item->{biblioitemnumber});
1289 if (my $data = $sth->fetchrow_hashref()){
1290 $itype = $data->{itemtype};
1296 =head2 AlterPriority
1298 AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1300 This function changes a reserve's priority up, down, to the top, or to the bottom.
1301 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1306 my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1308 my $hold = Koha::Holds->find( $reserve_id );
1309 return unless $hold;
1311 if ( $hold->cancellationdate ) {
1312 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1316 if ( $where eq 'up' ) {
1317 return unless $prev_priority;
1318 _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1319 } elsif ( $where eq 'down' ) {
1320 return unless $next_priority;
1321 _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1322 } elsif ( $where eq 'top' ) {
1323 _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1324 } elsif ( $where eq 'bottom' ) {
1325 _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1328 # FIXME Should return the new priority
1331 =head2 ToggleLowestPriority
1333 ToggleLowestPriority( $borrowernumber, $biblionumber );
1335 This function sets the lowestPriority field to true if is false, and false if it is true.
1339 sub ToggleLowestPriority {
1340 my ( $reserve_id ) = @_;
1342 my $dbh = C4::Context->dbh;
1344 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1345 $sth->execute( $reserve_id );
1347 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1350 =head2 ToggleSuspend
1352 ToggleSuspend( $reserve_id );
1354 This function sets the suspend field to true if is false, and false if it is true.
1355 If the reserve is currently suspended with a suspend_until date, that date will
1356 be cleared when it is unsuspended.
1361 my ( $reserve_id, $suspend_until ) = @_;
1363 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1365 my $hold = Koha::Holds->find( $reserve_id );
1367 if ( $hold->is_suspended ) {
1370 $hold->suspend_hold( $suspend_until );
1377 borrowernumber => $borrowernumber,
1378 [ biblionumber => $biblionumber, ]
1379 [ suspend_until => $suspend_until, ]
1380 [ suspend => $suspend ]
1383 This function accepts a set of hash keys as its parameters.
1384 It requires either borrowernumber or biblionumber, or both.
1386 suspend_until is wholly optional.
1393 my $borrowernumber = $params{'borrowernumber'} || undef;
1394 my $biblionumber = $params{'biblionumber'} || undef;
1395 my $suspend_until = $params{'suspend_until'} || undef;
1396 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1398 $suspend_until = eval { dt_from_string($suspend_until) }
1399 if ( defined($suspend_until) );
1401 return unless ( $borrowernumber || $biblionumber );
1404 $params->{found} = undef;
1405 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1406 $params->{biblionumber} = $biblionumber if $biblionumber;
1408 my @holds = Koha::Holds->search($params);
1411 map { $_->suspend_hold($suspend_until) } @holds;
1414 map { $_->resume() } @holds;
1422 reserve_id => $reserve_id,
1424 [ignoreSetLowestRank => $ignoreSetLowestRank]
1429 _FixPriority({ biblionumber => $biblionumber});
1431 This routine adjusts the priority of a hold request and holds
1434 In the first form, where a reserve_id is passed, the priority of the
1435 hold is set to supplied rank, and other holds for that bib are adjusted
1436 accordingly. If the rank is "del", the hold is cancelled. If no rank
1437 is supplied, all of the holds on that bib have their priority adjusted
1438 as if the second form had been used.
1440 In the second form, where a biblionumber is passed, the holds on that
1441 bib (that are not captured) are sorted in order of increasing priority,
1442 then have reserves.priority set so that the first non-captured hold
1443 has its priority set to 1, the second non-captured hold has its priority
1444 set to 2, and so forth.
1446 In both cases, holds that have the lowestPriority flag on are have their
1447 priority adjusted to ensure that they remain at the end of the line.
1449 Note that the ignoreSetLowestRank parameter is meant to be used only
1450 when _FixPriority calls itself.
1455 my ( $params ) = @_;
1456 my $reserve_id = $params->{reserve_id};
1457 my $rank = $params->{rank} // '';
1458 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1459 my $biblionumber = $params->{biblionumber};
1461 my $dbh = C4::Context->dbh;
1464 if ( $reserve_id ) {
1465 $hold = Koha::Holds->find( $reserve_id );
1466 return unless $hold;
1469 unless ( $biblionumber ) { # FIXME This is a very weird API
1470 $biblionumber = $hold->biblionumber;
1473 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1476 elsif ( $reserve_id && ( $rank eq "W" || $rank eq "0" ) ) {
1478 # make sure priority for waiting or in-transit items is 0
1482 WHERE reserve_id = ?
1483 AND found IN ('W', 'T')
1485 my $sth = $dbh->prepare($query);
1486 $sth->execute( $reserve_id );
1492 SELECT reserve_id, borrowernumber, reservedate
1494 WHERE biblionumber = ?
1495 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1496 ORDER BY priority ASC
1498 my $sth = $dbh->prepare($query);
1499 $sth->execute( $biblionumber );
1500 while ( my $line = $sth->fetchrow_hashref ) {
1501 push( @priority, $line );
1504 # FIXME This whole sub must be rewritten, especially to highlight what is done when reserve_id is not given
1505 # To find the matching index
1507 my $key = -1; # to allow for 0 to be a valid result
1508 for ( $i = 0 ; $i < @priority ; $i++ ) {
1509 if ( $reserve_id && $reserve_id == $priority[$i]->{'reserve_id'} ) {
1510 $key = $i; # save the index
1515 # if index exists in array then move it to new position
1516 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1517 my $new_rank = $rank -
1518 1; # $new_rank is what you want the new index to be in the array
1519 my $moving_item = splice( @priority, $key, 1 );
1520 splice( @priority, $new_rank, 0, $moving_item );
1523 # now fix the priority on those that are left....
1527 WHERE reserve_id = ?
1529 $sth = $dbh->prepare($query);
1530 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1533 $priority[$j]->{'reserve_id'}
1537 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1540 unless ( $ignoreSetLowestRank ) {
1541 while ( my $res = $sth->fetchrow_hashref() ) {
1543 reserve_id => $res->{'reserve_id'},
1545 ignoreSetLowestRank => 1
1551 =head2 _Findgroupreserve
1553 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1555 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1556 first match found. If neither, then we look for non-holds-queue based holds.
1557 Lookahead is the number of days to look in advance.
1559 C<&_Findgroupreserve> returns :
1560 C<@results> is an array of references-to-hash whose keys are mostly
1561 fields from the reserves table of the Koha database, plus
1562 C<biblioitemnumber>.
1564 This routine with either return:
1565 1 - Item specific holds from the holds queue
1566 2 - Title level holds from the holds queue
1567 3 - All holds for this biblionumber
1569 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1573 sub _Findgroupreserve {
1574 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1575 my $dbh = C4::Context->dbh;
1577 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1578 # check for exact targeted match
1579 my $item_level_target_query = qq{
1580 SELECT reserves.biblionumber AS biblionumber,
1581 reserves.borrowernumber AS borrowernumber,
1582 reserves.reservedate AS reservedate,
1583 reserves.branchcode AS branchcode,
1584 reserves.cancellationdate AS cancellationdate,
1585 reserves.found AS found,
1586 reserves.reservenotes AS reservenotes,
1587 reserves.priority AS priority,
1588 reserves.timestamp AS timestamp,
1589 biblioitems.biblioitemnumber AS biblioitemnumber,
1590 reserves.itemnumber AS itemnumber,
1591 reserves.reserve_id AS reserve_id,
1592 reserves.itemtype AS itemtype
1594 JOIN biblioitems USING (biblionumber)
1595 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1598 AND item_level_request = 1
1600 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1604 my $sth = $dbh->prepare($item_level_target_query);
1605 $sth->execute($itemnumber, $lookahead||0);
1607 if ( my $data = $sth->fetchrow_hashref ) {
1608 push( @results, $data )
1609 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1611 return @results if @results;
1613 # check for title-level targeted match
1614 my $title_level_target_query = qq{
1615 SELECT reserves.biblionumber AS biblionumber,
1616 reserves.borrowernumber AS borrowernumber,
1617 reserves.reservedate AS reservedate,
1618 reserves.branchcode AS branchcode,
1619 reserves.cancellationdate AS cancellationdate,
1620 reserves.found AS found,
1621 reserves.reservenotes AS reservenotes,
1622 reserves.priority AS priority,
1623 reserves.timestamp AS timestamp,
1624 biblioitems.biblioitemnumber AS biblioitemnumber,
1625 reserves.itemnumber AS itemnumber,
1626 reserves.reserve_id AS reserve_id,
1627 reserves.itemtype AS itemtype
1629 JOIN biblioitems USING (biblionumber)
1630 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1633 AND item_level_request = 0
1634 AND hold_fill_targets.itemnumber = ?
1635 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1639 $sth = $dbh->prepare($title_level_target_query);
1640 $sth->execute($itemnumber, $lookahead||0);
1642 if ( my $data = $sth->fetchrow_hashref ) {
1643 push( @results, $data )
1644 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1646 return @results if @results;
1649 SELECT reserves.biblionumber AS biblionumber,
1650 reserves.borrowernumber AS borrowernumber,
1651 reserves.reservedate AS reservedate,
1652 reserves.waitingdate AS waitingdate,
1653 reserves.branchcode AS branchcode,
1654 reserves.cancellationdate AS cancellationdate,
1655 reserves.found AS found,
1656 reserves.reservenotes AS reservenotes,
1657 reserves.priority AS priority,
1658 reserves.timestamp AS timestamp,
1659 reserves.itemnumber AS itemnumber,
1660 reserves.reserve_id AS reserve_id,
1661 reserves.itemtype AS itemtype
1663 WHERE reserves.biblionumber = ?
1664 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1665 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1669 $sth = $dbh->prepare($query);
1670 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1672 while ( my $data = $sth->fetchrow_hashref ) {
1673 push( @results, $data )
1674 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1679 =head2 _koha_notify_reserve
1681 _koha_notify_reserve( $hold->reserve_id );
1683 Sends a notification to the patron that their hold has been filled (through
1684 ModReserveAffect, _not_ ModReserveFill)
1686 The letter code for this notice may be found using the following query:
1688 select distinct letter_code
1689 from message_transports
1690 inner join message_attributes using (message_attribute_id)
1691 where message_name = 'Hold_Filled'
1693 This will probably sipmly be 'HOLD', but because it is defined in the database,
1694 it is subject to addition or change.
1696 The following tables are availalbe witin the notice:
1707 sub _koha_notify_reserve {
1708 my $reserve_id = shift;
1709 my $hold = Koha::Holds->find($reserve_id);
1710 my $borrowernumber = $hold->borrowernumber;
1712 my $patron = Koha::Patrons->find( $borrowernumber );
1714 # Try to get the borrower's email address
1715 my $to_address = $patron->notice_email_address;
1717 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1718 borrowernumber => $borrowernumber,
1719 message_name => 'Hold_Filled'
1722 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1724 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1726 my %letter_params = (
1727 module => 'reserves',
1728 branchcode => $hold->branchcode,
1729 lang => $patron->lang,
1731 'branches' => $library,
1732 'borrowers' => $patron->unblessed,
1733 'biblio' => $hold->biblionumber,
1734 'biblioitems' => $hold->biblionumber,
1735 'reserves' => $hold->unblessed,
1736 'items' => $hold->itemnumber,
1740 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.
1741 my $send_notification = sub {
1742 my ( $mtt, $letter_code ) = (@_);
1743 return unless defined $letter_code;
1744 $letter_params{letter_code} = $letter_code;
1745 $letter_params{message_transport_type} = $mtt;
1746 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1748 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1752 C4::Letters::EnqueueLetter( {
1754 borrowernumber => $borrowernumber,
1755 from_address => $admin_email_address,
1756 message_transport_type => $mtt,
1760 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1762 ( $mtt eq 'email' and not $to_address ) # No email address
1763 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1764 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1767 &$send_notification($mtt, $letter_code);
1768 $notification_sent++;
1770 #Making sure that a print notification is sent if no other transport types can be utilized.
1771 if (! $notification_sent) {
1772 &$send_notification('print', 'HOLD');
1777 =head2 _ShiftPriorityByDateAndPriority
1779 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1781 This increments the priority of all reserves after the one
1782 with either the lowest date after C<$reservedate>
1783 or the lowest priority after C<$priority>.
1785 It effectively makes room for a new reserve to be inserted with a certain
1786 priority, which is returned.
1788 This is most useful when the reservedate can be set by the user. It allows
1789 the new reserve to be placed before other reserves that have a later
1790 reservedate. Since priority also is set by the form in reserves/request.pl
1791 the sub accounts for that too.
1795 sub _ShiftPriorityByDateAndPriority {
1796 my ( $biblio, $resdate, $new_priority ) = @_;
1798 my $dbh = C4::Context->dbh;
1799 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1800 my $sth = $dbh->prepare( $query );
1801 $sth->execute( $biblio, $resdate, $new_priority );
1802 my $min_priority = $sth->fetchrow;
1803 # if no such matches are found, $new_priority remains as original value
1804 $new_priority = $min_priority if ( $min_priority );
1806 # Shift the priority up by one; works in conjunction with the next SQL statement
1807 $query = "UPDATE reserves
1808 SET priority = priority+1
1809 WHERE biblionumber = ?
1810 AND borrowernumber = ?
1813 my $sth_update = $dbh->prepare( $query );
1815 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1816 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1817 $sth = $dbh->prepare( $query );
1818 $sth->execute( $new_priority, $biblio );
1819 while ( my $row = $sth->fetchrow_hashref ) {
1820 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1823 return $new_priority; # so the caller knows what priority they wind up receiving
1828 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1830 Use when checking out an item to handle reserves
1831 If $cancelreserve boolean is set to true, it will remove existing reserve
1836 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1838 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1839 my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1842 my $biblionumber = $res->{biblionumber};
1844 if ($res->{borrowernumber} == $borrowernumber) {
1845 ModReserveFill($res);
1849 # The item is reserved by someone else.
1850 # Find this item in the reserves
1852 my $borr_res = Koha::Holds->search({
1853 borrowernumber => $borrowernumber,
1854 biblionumber => $biblionumber,
1856 order_by => 'priority'
1860 # The item is reserved by the current patron
1861 ModReserveFill($borr_res->unblessed);
1864 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1865 RevertWaitingStatus({ itemnumber => $itemnumber });
1867 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1868 my $hold = Koha::Holds->find( $res->{reserve_id} );
1876 MergeHolds($dbh,$to_biblio, $from_biblio);
1878 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1883 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1884 my $sth = $dbh->prepare(
1885 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1887 $sth->execute($from_biblio);
1888 if ( my $data = $sth->fetchrow_hashref() ) {
1890 # holds exist on old record, if not we don't need to do anything
1891 $sth = $dbh->prepare(
1892 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1893 $sth->execute( $to_biblio, $from_biblio );
1896 # don't reorder those already waiting
1898 $sth = $dbh->prepare(
1899 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1901 my $upd_sth = $dbh->prepare(
1902 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1903 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1905 $sth->execute( $to_biblio, 'W', 'T' );
1907 while ( my $reserve = $sth->fetchrow_hashref() ) {
1909 $priority, $to_biblio,
1910 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1911 $reserve->{'itemnumber'}
1918 =head2 RevertWaitingStatus
1920 RevertWaitingStatus({ itemnumber => $itemnumber });
1922 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1924 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1925 item level hold, even if it was only a bibliolevel hold to
1926 begin with. This is because we can no longer know if a hold
1927 was item-level or bib-level after a hold has been set to
1932 sub RevertWaitingStatus {
1933 my ( $params ) = @_;
1934 my $itemnumber = $params->{'itemnumber'};
1936 return unless ( $itemnumber );
1938 my $dbh = C4::Context->dbh;
1940 ## Get the waiting reserve we want to revert
1942 SELECT * FROM reserves
1943 WHERE itemnumber = ?
1944 AND found IS NOT NULL
1946 my $sth = $dbh->prepare( $query );
1947 $sth->execute( $itemnumber );
1948 my $reserve = $sth->fetchrow_hashref();
1950 my $hold = Koha::Holds->find( $reserve->{reserve_id} ); # TODO Remove the next raw SQL statements and use this instead
1952 ## Increment the priority of all other non-waiting
1953 ## reserves for this bib record
1957 priority = priority + 1
1963 $sth = $dbh->prepare( $query );
1964 $sth->execute( $reserve->{'biblionumber'} );
1966 ## Fix up the currently waiting reserve
1976 $sth = $dbh->prepare( $query );
1977 $sth->execute( $reserve->{'reserve_id'} );
1979 unless ( $hold->item_level_hold ) {
1980 $hold->itemnumber(undef)->store;
1983 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1990 branchcode => $branchcode,
1991 borrowernumber => $borrowernumber,
1992 biblionumber => $biblionumber,
1993 [ itemnumber => $itemnumber, ]
1994 [ barcode => $barcode, ]
1998 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2000 The letter code will be HOLD_SLIP, and the following tables are
2001 available within the slip:
2014 my $branchcode = $args->{branchcode};
2015 my $borrowernumber = $args->{borrowernumber};
2016 my $biblionumber = $args->{biblionumber};
2017 my $itemnumber = $args->{itemnumber};
2018 my $barcode = $args->{barcode};
2021 my $patron = Koha::Patrons->find($borrowernumber);
2024 if ($itemnumber || $barcode ) {
2025 $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
2027 $hold = Koha::Holds->search(
2029 biblionumber => $biblionumber,
2030 borrowernumber => $borrowernumber,
2031 itemnumber => $itemnumber
2036 $hold = Koha::Holds->search(
2038 biblionumber => $biblionumber,
2039 borrowernumber => $borrowernumber
2044 return unless $hold;
2045 my $reserve = $hold->unblessed;
2047 return C4::Letters::GetPreparedLetter (
2048 module => 'circulation',
2049 letter_code => 'HOLD_SLIP',
2050 branchcode => $branchcode,
2051 lang => $patron->lang,
2053 'reserves' => $reserve,
2054 'branches' => $reserve->{branchcode},
2055 'borrowers' => $reserve->{borrowernumber},
2056 'biblio' => $reserve->{biblionumber},
2057 'biblioitems' => $reserve->{biblionumber},
2058 'items' => $reserve->{itemnumber},
2063 =head2 GetReservesControlBranch
2065 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2067 Return the branchcode to be used to determine which reserves
2068 policy applies to a transaction.
2070 C<$item> is a hashref for an item. Only 'homebranch' is used.
2072 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2076 sub GetReservesControlBranch {
2077 my ( $item, $borrower ) = @_;
2079 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2082 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2083 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2089 =head2 CalculatePriority
2091 my $p = CalculatePriority($biblionumber, $resdate);
2093 Calculate priority for a new reserve on biblionumber, placing it at
2094 the end of the line of all holds whose start date falls before
2095 the current system time and that are neither on the hold shelf
2098 The reserve date parameter is optional; if it is supplied, the
2099 priority is based on the set of holds whose start date falls before
2100 the parameter value.
2102 After calculation of this priority, it is recommended to call
2103 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2108 sub CalculatePriority {
2109 my ( $biblionumber, $resdate ) = @_;
2112 SELECT COUNT(*) FROM reserves
2113 WHERE biblionumber = ?
2115 AND (found IS NULL OR found = '')
2117 #skip found==W or found==T (waiting or transit holds)
2119 $sql.= ' AND ( reservedate <= ? )';
2122 $sql.= ' AND ( reservedate < NOW() )';
2124 my $dbh = C4::Context->dbh();
2125 my @row = $dbh->selectrow_array(
2128 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2131 return @row ? $row[0]+1 : 1;
2134 =head2 IsItemOnHoldAndFound
2136 my $bool = IsItemFoundHold( $itemnumber );
2138 Returns true if the item is currently on hold
2139 and that hold has a non-null found status ( W, T, etc. )
2143 sub IsItemOnHoldAndFound {
2144 my ($itemnumber) = @_;
2146 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2148 my $found = $rs->count(
2150 itemnumber => $itemnumber,
2151 found => { '!=' => undef }
2158 =head2 GetMaxPatronHoldsForRecord
2160 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2162 For multiple holds on a given record for a given patron, the max
2163 number of record level holds that a patron can be placed is the highest
2164 value of the holds_per_record rule for each item if the record for that
2165 patron. This subroutine finds and returns the highest holds_per_record
2166 rule value for a given patron id and record id.
2170 sub GetMaxPatronHoldsForRecord {
2171 my ( $borrowernumber, $biblionumber ) = @_;
2173 my $patron = Koha::Patrons->find($borrowernumber);
2174 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2176 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2178 my $categorycode = $patron->categorycode;
2180 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2183 foreach my $item (@items) {
2184 my $itemtype = $item->effective_itemtype();
2186 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2188 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2189 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2190 $max = $holds_per_record if $holds_per_record > $max;
2198 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2200 Returns the matching hold related issuingrule fields for a given
2201 patron category, itemtype, and library.
2206 my ( $categorycode, $itemtype, $branchcode ) = @_;
2208 my $reservesallowed = Koha::CirculationRules->get_effective_rule(
2210 itemtype => $itemtype,
2211 categorycode => $categorycode,
2212 branchcode => $branchcode,
2213 rule_name => 'reservesallowed',
2215 -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
2219 return unless $reservesallowed;;
2222 $rules->{reservesallowed} = $reservesallowed->rule_value;
2223 $rules->{itemtype} = $reservesallowed->itemtype;
2224 $rules->{categorycode} = $reservesallowed->categorycode;
2225 $rules->{branchcode} = $reservesallowed->branchcode;
2227 my $holds_per_x_rules = Koha::CirculationRules->get_effective_rules(
2229 itemtype => $itemtype,
2230 categorycode => $categorycode,
2231 branchcode => $branchcode,
2232 rules => ['holds_per_record', 'holds_per_day'],
2234 -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
2238 $rules->{holds_per_record} = $holds_per_x_rules->{holds_per_record};
2239 $rules->{holds_per_day} = $holds_per_x_rules->{holds_per_day};
2246 Koha Development Team <http://koha-community.org/>