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
125 ItemsAnyAvailableAndNotRestricted
128 &ToggleLowestPriority
134 &GetReservesControlBranch
138 GetMaxPatronHoldsForRecord
140 @EXPORT_OK = qw( MergeHolds );
147 branchcode => $branchcode,
148 borrowernumber => $borrowernumber,
149 biblionumber => $biblionumber,
150 priority => $priority,
151 reservation_date => $reservation_date,
152 expiration_date => $expiration_date,
155 itemnumber => $itemnumber,
157 itemtype => $itemtype,
161 Adds reserve and generates HOLDPLACED message.
163 The following tables are available witin the HOLDPLACED message:
176 my $branch = $params->{branchcode};
177 my $borrowernumber = $params->{borrowernumber};
178 my $biblionumber = $params->{biblionumber};
179 my $priority = $params->{priority};
180 my $resdate = $params->{reservation_date};
181 my $expdate = $params->{expiration_date};
182 my $notes = $params->{notes};
183 my $title = $params->{title};
184 my $checkitem = $params->{itemnumber};
185 my $found = $params->{found};
186 my $itemtype = $params->{itemtype};
188 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
189 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
191 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
193 # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
194 # of the document, we force the value $priority and $found .
195 if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
196 my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
199 # If item is already checked out, it cannot be set waiting
202 # The item can't be waiting if it needs a transfer
203 && $item->holdingbranch eq $branch
205 # Similarly, if in transit it can't be waiting
206 && !$item->get_transfer
208 # If we can't hold damaged items, and it is damaged, it can't be waiting
209 && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
211 # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
212 && !$item->current_holds->count )
219 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
221 # Make room in reserves for this before those of a later reserve date
222 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
227 # If the reserv had the waiting status, we had the value of the resdate
228 if ( $found && $found eq 'W' ) {
229 $waitingdate = $resdate;
232 # Don't add itemtype limit if specific item is selected
233 $itemtype = undef if $checkitem;
235 # updates take place here
236 my $hold = Koha::Hold->new(
238 borrowernumber => $borrowernumber,
239 biblionumber => $biblionumber,
240 reservedate => $resdate,
241 branchcode => $branch,
242 priority => $priority,
243 reservenotes => $notes,
244 itemnumber => $checkitem,
246 waitingdate => $waitingdate,
247 expirationdate => $expdate,
248 itemtype => $itemtype,
249 item_level_hold => $checkitem ? 1 : 0,
252 $hold->set_waiting() if $found && $found eq 'W';
254 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
255 if C4::Context->preference('HoldsLog');
257 my $reserve_id = $hold->id();
259 # add a reserve fee if needed
260 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
261 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
262 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
265 _FixPriority({ biblionumber => $biblionumber});
267 # Send e-mail to librarian if syspref is active
268 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
269 my $patron = Koha::Patrons->find( $borrowernumber );
270 my $library = $patron->library;
271 if ( my $letter = C4::Letters::GetPreparedLetter (
272 module => 'reserves',
273 letter_code => 'HOLDPLACED',
274 branchcode => $branch,
275 lang => $patron->lang,
277 'branches' => $library->unblessed,
278 'borrowers' => $patron->unblessed,
279 'biblio' => $biblionumber,
280 'biblioitems' => $biblionumber,
281 'items' => $checkitem,
282 'reserves' => $hold->unblessed,
286 my $branch_email_address = $library->inbound_email_address;
288 C4::Letters::EnqueueLetter(
291 borrowernumber => $borrowernumber,
292 message_transport_type => 'email',
293 to_address => $branch_email_address,
302 =head2 CanBookBeReserved
304 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode, $params)
305 if ($canReserve eq 'OK') { #We can reserve this Item! }
307 $params are passed directly through to CanItemBeReserved
309 See CanItemBeReserved() for possible return values.
313 sub CanBookBeReserved{
314 my ($borrowernumber, $biblionumber, $pickup_branchcode, $params) = @_;
316 my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
317 #get items linked via host records
318 my @hostitems = get_hostitemnumbers_of($biblionumber);
320 push (@itemnumbers, @hostitems);
323 my $canReserve = { status => '' };
324 foreach my $itemnumber (@itemnumbers) {
325 $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode, $params );
326 return { status => 'OK' } if $canReserve->{status} eq 'OK';
331 =head2 CanItemBeReserved
333 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode, $params)
334 if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
336 current params are 'ignore_found_holds' - if true holds that have been trapped are not counted
337 toward the patron limit, used by checkHighHolds to avoid counting the hold we will fill with the
338 current checkout against the high holds threshold
340 @RETURNS { status => OK }, if the Item can be reserved.
341 { status => ageRestricted }, if the Item is age restricted for this borrower.
342 { status => damaged }, if the Item is damaged.
343 { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
344 { status => branchNotInHoldGroup }, if borrower home library is not in hold group, and holds are only allowed from hold groups.
345 { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
346 { status => notReservable }, if holds on this item are not allowed
347 { status => libraryNotFound }, if given branchcode is not an existing library
348 { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location
349 { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
350 { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
354 sub CanItemBeReserved {
355 my ( $borrowernumber, $itemnumber, $pickup_branchcode, $params ) = @_;
357 my $dbh = C4::Context->dbh;
358 my $ruleitemtype; # itemtype of the matching issuing rule
359 my $allowedreserves = 0; # Total number of holds allowed across all records, default to none
361 # we retrieve borrowers and items informations #
362 # item->{itype} will come for biblioitems if necessery
363 my $item = Koha::Items->find($itemnumber);
364 my $biblio = $item->biblio;
365 my $patron = Koha::Patrons->find( $borrowernumber );
366 my $borrower = $patron->unblessed;
368 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
369 return { status =>'damaged' }
371 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
373 # Check for the age restriction
374 my ( $ageRestriction, $daysToAgeRestriction ) =
375 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
376 return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
378 # Check that the patron doesn't have an item level hold on this item already
379 return { status =>'itemAlreadyOnHold' }
380 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
382 my $controlbranch = C4::Context->preference('ReservesControlBranch');
385 SELECT count(*) AS count
387 LEFT JOIN items USING (itemnumber)
388 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
389 LEFT JOIN borrowers USING (borrowernumber)
390 WHERE borrowernumber = ?
394 my $branchfield = "reserves.branchcode";
396 if ( $controlbranch eq "ItemHomeLibrary" ) {
397 $branchfield = "items.homebranch";
398 $branchcode = $item->homebranch;
400 elsif ( $controlbranch eq "PatronLibrary" ) {
401 $branchfield = "borrowers.branchcode";
402 $branchcode = $borrower->{branchcode};
407 my $reservesallowed = Koha::CirculationRules->get_effective_rule({
408 itemtype => $item->effective_itemtype,
409 categorycode => $borrower->{categorycode},
410 branchcode => $branchcode,
411 rule_name => 'reservesallowed',
414 $ruleitemtype = $reservesallowed->itemtype;
415 $allowedreserves = $reservesallowed->rule_value // 0; #undefined is 0, blank is unlimited
418 $ruleitemtype = undef;
421 my $rights = Koha::CirculationRules->get_effective_rules({
422 categorycode => $borrower->{'categorycode'},
423 itemtype => $item->effective_itemtype,
424 branchcode => $branchcode,
425 rules => ['holds_per_record','holds_per_day']
427 my $holds_per_record = $rights->{holds_per_record} // 1;
428 my $holds_per_day = $rights->{holds_per_day};
430 my $search_params = {
431 borrowernumber => $borrowernumber,
432 biblionumber => $item->biblionumber,
434 $search_params->{found} = undef if $params->{ignore_found_holds};
436 my $holds = Koha::Holds->search($search_params);
437 if ( defined $holds_per_record && $holds_per_record ne ''
438 && $holds->count() >= $holds_per_record ) {
439 return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
442 my $today_holds = Koha::Holds->search({
443 borrowernumber => $borrowernumber,
444 reservedate => dt_from_string->date
447 if ( defined $holds_per_day && $holds_per_day ne ''
448 && $today_holds->count() >= $holds_per_day )
450 return { status => 'tooManyReservesToday', limit => $holds_per_day };
455 $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
457 # If using item-level itypes, fall back to the record
458 # level itemtype if the hold has no associated item
460 C4::Context->preference('item-level_itypes')
461 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
462 : " AND biblioitems.itemtype = ?"
463 if defined $ruleitemtype;
465 my $sthcount = $dbh->prepare($querycount);
467 if ( defined $ruleitemtype ) {
468 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
471 $sthcount->execute( $borrowernumber, $branchcode );
474 my $reservecount = "0";
475 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
476 $reservecount = $rowcount->{count};
479 # we check if it's ok or not
480 if ( defined $allowedreserves && $allowedreserves ne ''
481 && $reservecount >= $allowedreserves ) {
482 return { status => 'tooManyReserves', limit => $allowedreserves };
485 # Now we need to check hold limits by patron category
486 my $rule = Koha::CirculationRules->get_effective_rule(
488 categorycode => $borrower->{categorycode},
489 branchcode => $branchcode,
490 rule_name => 'max_holds',
493 if ( $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
494 my $total_holds_count = Koha::Holds->search(
496 borrowernumber => $borrower->{borrowernumber}
500 return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
503 my $reserves_control_branch =
504 GetReservesControlBranch( $item->unblessed(), $borrower );
506 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype ); # FIXME Should not be item->effective_itemtype?
508 if ( $branchitemrule->{holdallowed} == 0 ) {
509 return { status => 'notReservable' };
512 if ( $branchitemrule->{holdallowed} == 1
513 && $borrower->{branchcode} ne $item->homebranch )
515 return { status => 'cannotReserveFromOtherBranches' };
518 my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
519 if ( $branchitemrule->{holdallowed} == 3) {
520 if($borrower->{branchcode} ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $borrower->{branchcode}} )) {
521 return { status => 'branchNotInHoldGroup' };
525 # If reservecount is ok, we check item branch if IndependentBranches is ON
526 # and canreservefromotherbranches is OFF
527 if ( C4::Context->preference('IndependentBranches')
528 and !C4::Context->preference('canreservefromotherbranches') )
530 if ( $item->homebranch ne $borrower->{branchcode} ) {
531 return { status => 'cannotReserveFromOtherBranches' };
535 if ($pickup_branchcode) {
536 my $destination = Koha::Libraries->find({
537 branchcode => $pickup_branchcode,
540 unless ($destination) {
541 return { status => 'libraryNotFound' };
543 unless ($destination->pickup_location) {
544 return { status => 'libraryNotPickupLocation' };
546 unless ($item->can_be_transferred({ to => $destination })) {
547 return { status => 'cannotBeTransferred' };
549 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup' && !$item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
550 return { status => 'pickupNotInHoldGroup' };
552 if ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup' && !Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
553 return { status => 'pickupNotInHoldGroup' };
557 return { status => 'OK' };
560 =head2 CanReserveBeCanceledFromOpac
562 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
564 returns 1 if reserve can be cancelled by user from OPAC.
565 First check if reserve belongs to user, next checks if reserve is not in
566 transfer or waiting status
570 sub CanReserveBeCanceledFromOpac {
571 my ($reserve_id, $borrowernumber) = @_;
573 return unless $reserve_id and $borrowernumber;
574 my $reserve = Koha::Holds->find($reserve_id);
576 return 0 unless $reserve->borrowernumber == $borrowernumber;
577 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
583 =head2 GetOtherReserves
585 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
587 Check queued list of this document and check if this document must be transferred
591 sub GetOtherReserves {
592 my ($itemnumber) = @_;
595 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
596 if ($checkreserves) {
597 my $item = Koha::Items->find($itemnumber);
598 if ( $item->holdingbranch ne $checkreserves->{'branchcode'} ) {
599 $messages->{'transfert'} = $checkreserves->{'branchcode'};
600 #minus priorities of others reservs
601 ModReserveMinusPriority(
603 $checkreserves->{'reserve_id'},
606 #launch the subroutine dotransfer
607 C4::Items::ModItemTransfer(
609 $item->holdingbranch,
610 $checkreserves->{'branchcode'},
616 #step 2b : case of a reservation on the same branch, set the waiting status
618 $messages->{'waiting'} = 1;
619 ModReserveMinusPriority(
621 $checkreserves->{'reserve_id'},
623 ModReserveStatus($itemnumber,'W');
626 $nextreservinfo = $checkreserves;
629 return ( $messages, $nextreservinfo );
632 =head2 ChargeReserveFee
634 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
636 Charge the fee for a reserve (if $fee > 0)
640 sub ChargeReserveFee {
641 my ( $borrowernumber, $fee, $title ) = @_;
642 return if !$fee || $fee == 0; # the last test is needed to include 0.00
643 Koha::Account->new( { patron_id => $borrowernumber } )->add_debit(
646 description => $title,
648 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
649 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
650 interface => C4::Context->interface,
651 invoice_type => undef,
660 $fee = GetReserveFee( $borrowernumber, $biblionumber );
662 Calculate the fee for a reserve (if applicable).
667 my ( $borrowernumber, $biblionumber ) = @_;
669 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
672 SELECT COUNT(*) FROM items
673 LEFT JOIN issues USING (itemnumber)
674 WHERE items.biblionumber=? AND issues.issue_id IS NULL
677 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
680 my $dbh = C4::Context->dbh;
681 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
682 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
683 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
684 # This is a reconstruction of the old code:
685 # Compare number of items with items issued, and optionally check holds
686 # If not all items are issued and there are no holds: charge no fee
687 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
688 my ( $notissued, $reserved );
689 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
692 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
693 ( $biblionumber, $borrowernumber ) );
694 $fee = 0 if $reserved == 0;
700 =head2 GetReserveStatus
702 $reservestatus = GetReserveStatus($itemnumber);
704 Takes an itemnumber and returns the status of the reserve placed on it.
705 If several reserves exist, the reserve with the lower priority is given.
709 ## FIXME: I don't think this does what it thinks it does.
710 ## It only ever checks the first reserve result, even though
711 ## multiple reserves for that bib can have the itemnumber set
712 ## the sub is only used once in the codebase.
713 sub GetReserveStatus {
714 my ($itemnumber) = @_;
716 my $dbh = C4::Context->dbh;
718 my ($sth, $found, $priority);
720 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
721 $sth->execute($itemnumber);
722 ($found, $priority) = $sth->fetchrow_array;
726 return 'Waiting' if $found eq 'W' and $priority == 0;
727 return 'Finished' if $found eq 'F';
730 return 'Reserved' if defined $priority && $priority > 0;
732 return ''; # empty string here will remove need for checking undef, or less log lines
737 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber);
738 ($status, $matched_reserve, $possible_reserves) = &CheckReserves(undef, $barcode);
739 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
741 Find a book in the reserves.
743 C<$itemnumber> is the book's item number.
744 C<$lookahead> is the number of days to look in advance for future reserves.
746 As I understand it, C<&CheckReserves> looks for the given item in the
747 reserves. If it is found, that's a match, and C<$status> is set to
750 Otherwise, it finds the most important item in the reserves with the
751 same biblio number as this book (I'm not clear on this) and returns it
752 with C<$status> set to C<Reserved>.
754 C<&CheckReserves> returns a two-element list:
756 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
758 C<$reserve> is the reserve item that matched. It is a
759 reference-to-hash whose keys are mostly the fields of the reserves
760 table in the Koha database.
765 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
766 my $dbh = C4::Context->dbh;
769 if (C4::Context->preference('item-level_itypes')){
771 SELECT items.biblionumber,
772 items.biblioitemnumber,
773 itemtypes.notforloan,
774 items.notforloan AS itemnotforloan,
780 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
781 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
786 SELECT items.biblionumber,
787 items.biblioitemnumber,
788 itemtypes.notforloan,
789 items.notforloan AS itemnotforloan,
795 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
796 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
801 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
802 $sth->execute($item);
805 $sth = $dbh->prepare("$select WHERE barcode = ?");
806 $sth->execute($barcode);
808 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
809 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
810 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
812 return unless $itemnumber; # bail if we got nothing.
813 # if item is not for loan it cannot be reserved either.....
814 # except where items.notforloan < 0 : This indicates the item is holdable.
816 my @SkipHoldTrapOnNotForLoanValue = split( '\|', C4::Context->preference('SkipHoldTrapOnNotForLoanValue') );
817 return if grep { $_ eq $notforloan_per_item } @SkipHoldTrapOnNotForLoanValue;
819 my $dont_trap = C4::Context->preference('TrapHoldsOnOrder') ? ($notforloan_per_item > 0) : ($notforloan_per_item && 1 );
820 return if $dont_trap or $notforloan_per_itemtype;
822 # Find this item in the reserves
823 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
825 # $priority and $highest are used to find the most important item
826 # in the list returned by &_Findgroupreserve. (The lower $priority,
827 # the more important the item.)
828 # $highest is the most important item we've seen so far.
831 if (scalar @reserves) {
832 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
833 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
834 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
836 my $priority = 10000000;
837 foreach my $res (@reserves) {
838 if ( $res->{'itemnumber'} && $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
839 if ($res->{'found'} eq 'W') {
840 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
842 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
847 my $local_hold_match;
849 if ($LocalHoldsPriority) {
850 $patron = Koha::Patrons->find( $res->{borrowernumber} );
851 $item = Koha::Items->find($itemnumber);
853 my $local_holds_priority_item_branchcode =
854 $item->$LocalHoldsPriorityItemControl;
855 my $local_holds_priority_patron_branchcode =
856 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
858 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
859 ? $patron->branchcode
862 $local_holds_priority_item_branchcode eq
863 $local_holds_priority_patron_branchcode;
866 # See if this item is more important than what we've got so far
867 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
868 $item ||= Koha::Items->find($itemnumber);
869 next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
870 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
871 my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
872 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
873 next if ($branchitemrule->{'holdallowed'} == 0);
874 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
875 my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
876 next if (($branchitemrule->{'holdallowed'} == 3) && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
877 my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
878 next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
879 next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
880 next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
881 next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
882 $priority = $res->{'priority'};
884 last if $local_hold_match;
890 # If we get this far, then no exact match was found.
891 # We return the most important (i.e. next) reservation.
893 $highest->{'itemnumber'} = $item;
894 return ( "Reserved", $highest, \@reserves );
900 =head2 CancelExpiredReserves
902 CancelExpiredReserves();
904 Cancels all reserves with an expiration date from before today.
908 sub CancelExpiredReserves {
909 my $today = dt_from_string();
910 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
911 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
913 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
914 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
915 $params->{found} = [ { '!=', 'W' }, undef ] unless $expireWaiting;
917 # FIXME To move to Koha::Holds->search_expired (?)
918 my $holds = Koha::Holds->search( $params );
920 while ( my $hold = $holds->next ) {
921 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
923 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
925 my $cancel_params = {};
926 if ( $hold->found eq 'W' ) {
927 $cancel_params->{charge_cancel_fee} = 1;
929 $hold->cancel( $cancel_params );
933 =head2 AutoUnsuspendReserves
935 AutoUnsuspendReserves();
937 Unsuspends all suspended reserves with a suspend_until date from before today.
941 sub AutoUnsuspendReserves {
942 my $today = dt_from_string();
944 my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
946 map { $_->resume() } @holds;
951 ModReserve({ rank => $rank,
952 reserve_id => $reserve_id,
953 branchcode => $branchcode
954 [, itemnumber => $itemnumber ]
955 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
958 Change a hold request's priority or cancel it.
960 C<$rank> specifies the effect of the change. If C<$rank>
961 is 'W' or 'n', nothing happens. This corresponds to leaving a
962 request alone when changing its priority in the holds queue
965 If C<$rank> is 'del', the hold request is cancelled.
967 If C<$rank> is an integer greater than zero, the priority of
968 the request is set to that value. Since priority != 0 means
969 that the item is not waiting on the hold shelf, setting the
970 priority to a non-zero value also sets the request's found
971 status and waiting date to NULL.
973 The optional C<$itemnumber> parameter is used only when
974 C<$rank> is a non-zero integer; if supplied, the itemnumber
975 of the hold request is set accordingly; if omitted, the itemnumber
978 B<FIXME:> Note that the forgoing can have the effect of causing
979 item-level hold requests to turn into title-level requests. This
980 will be fixed once reserves has separate columns for requested
981 itemnumber and supplying itemnumber.
988 my $rank = $params->{'rank'};
989 my $reserve_id = $params->{'reserve_id'};
990 my $branchcode = $params->{'branchcode'};
991 my $itemnumber = $params->{'itemnumber'};
992 my $suspend_until = $params->{'suspend_until'};
993 my $borrowernumber = $params->{'borrowernumber'};
994 my $biblionumber = $params->{'biblionumber'};
996 return if $rank eq "W";
997 return if $rank eq "n";
999 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1002 unless ( $reserve_id ) {
1003 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
1004 return unless $holds->count; # FIXME Should raise an exception
1005 $hold = $holds->next;
1006 $reserve_id = $hold->reserve_id;
1009 $hold ||= Koha::Holds->find($reserve_id);
1011 if ( $rank eq "del" ) {
1014 elsif ($rank =~ /^\d+/ and $rank > 0) {
1015 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1016 if C4::Context->preference('HoldsLog');
1020 branchcode => $branchcode,
1021 itemnumber => $itemnumber,
1023 waitingdate => undef
1025 if (exists $params->{reservedate}) {
1026 $properties->{reservedate} = $params->{reservedate} || undef;
1028 if (exists $params->{expirationdate}) {
1029 $properties->{expirationdate} = $params->{expirationdate} || undef;
1032 $hold->set($properties)->store();
1034 if ( defined( $suspend_until ) ) {
1035 if ( $suspend_until ) {
1036 $suspend_until = eval { dt_from_string( $suspend_until ) };
1037 $hold->suspend_hold( $suspend_until );
1039 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1040 # If the hold is not suspended, this does nothing.
1041 $hold->set( { suspend_until => undef } )->store();
1045 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1049 =head2 ModReserveFill
1051 &ModReserveFill($reserve);
1053 Fill a reserve. If I understand this correctly, this means that the
1054 reserved book has been found and given to the patron who reserved it.
1056 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1057 whose keys are fields from the reserves table in the Koha database.
1061 sub ModReserveFill {
1063 my $reserve_id = $res->{'reserve_id'};
1065 my $hold = Koha::Holds->find($reserve_id);
1066 # get the priority on this record....
1067 my $priority = $hold->priority;
1069 # update the hold statuses, no need to store it though, we will be deleting it anyway
1077 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1078 if C4::Context->preference('HoldsLog');
1080 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
1081 Koha::Old::Hold->new( $hold->unblessed() )->store();
1085 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1086 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1087 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1090 # now fix the priority on the others (if the priority wasn't
1091 # already sorted!)....
1092 unless ( $priority == 0 ) {
1093 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1097 =head2 ModReserveStatus
1099 &ModReserveStatus($itemnumber, $newstatus);
1101 Update the reserve status for the active (priority=0) reserve.
1103 $itemnumber is the itemnumber the reserve is on
1105 $newstatus is the new status.
1109 sub ModReserveStatus {
1111 #first : check if we have a reservation for this item .
1112 my ($itemnumber, $newstatus) = @_;
1113 my $dbh = C4::Context->dbh;
1115 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1116 my $sth_set = $dbh->prepare($query);
1117 $sth_set->execute( $newstatus, $itemnumber );
1119 my $item = Koha::Items->find($itemnumber);
1120 if ( $item->location && $item->location eq 'CART'
1121 && ( !$item->permanent_location || $item->permanent_location ne 'CART' )
1123 CartToShelf( $itemnumber );
1127 =head2 ModReserveAffect
1129 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1131 This function affect an item and a status for a given reserve, either fetched directly
1132 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1133 is given, only first reserve returned is affected, which is ok for anything but
1136 if $transferToDo is not set, then the status is set to "Waiting" as well.
1137 otherwise, a transfer is on the way, and the end of the transfer will
1138 take care of the waiting status
1142 sub ModReserveAffect {
1143 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1144 my $dbh = C4::Context->dbh;
1146 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1147 # attached to $itemnumber
1148 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1149 $sth->execute($itemnumber);
1150 my ($biblionumber) = $sth->fetchrow;
1152 # get request - need to find out if item is already
1153 # waiting in order to not send duplicate hold filled notifications
1156 # Find hold by id if we have it
1157 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1158 # Find item level hold for this item if there is one
1159 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1160 # Find record level hold if there is no item level hold
1161 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1163 return unless $hold;
1165 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1167 $hold->itemnumber($itemnumber);
1168 $hold->set_waiting($transferToDo);
1170 if( !$transferToDo ){
1171 _koha_notify_reserve( $hold->reserve_id ) unless $already_on_shelf;
1172 my $transfers = Koha::Item::Transfers->search({
1173 itemnumber => $itemnumber,
1174 datearrived => undef
1176 while( my $transfer = $transfers->next ){
1177 $transfer->datearrived( dt_from_string() )->store;
1182 _FixPriority( { biblionumber => $biblionumber } );
1183 my $item = Koha::Items->find($itemnumber);
1184 if ( $item->location && $item->location eq 'CART'
1185 && ( !$item->permanent_location || $item->permanent_location ne 'CART' ) ) {
1186 CartToShelf( $itemnumber );
1189 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->get_from_storage->unblessed) )
1190 if C4::Context->preference('HoldsLog');
1195 =head2 ModReserveCancelAll
1197 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1199 function to cancel reserv,check other reserves, and transfer document if it's necessary
1203 sub ModReserveCancelAll {
1206 my ( $itemnumber, $borrowernumber ) = @_;
1208 #step 1 : cancel the reservation
1209 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1210 return unless $holds->count;
1211 $holds->next->cancel;
1213 #step 2 launch the subroutine of the others reserves
1214 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1216 return ( $messages, $nextreservinfo->{borrowernumber} );
1219 =head2 ModReserveMinusPriority
1221 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1223 Reduce the values of queued list
1227 sub ModReserveMinusPriority {
1228 my ( $itemnumber, $reserve_id ) = @_;
1230 #first step update the value of the first person on reserv
1231 my $dbh = C4::Context->dbh;
1234 SET priority = 0 , itemnumber = ?
1235 WHERE reserve_id = ?
1237 my $sth_upd = $dbh->prepare($query);
1238 $sth_upd->execute( $itemnumber, $reserve_id );
1239 # second step update all others reserves
1240 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1243 =head2 IsAvailableForItemLevelRequest
1245 my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1247 Checks whether a given item record is available for an
1248 item-level hold request. An item is available if
1250 * it is not lost AND
1251 * it is not damaged AND
1252 * it is not withdrawn AND
1253 * a waiting or in transit reserve is placed on
1254 * does not have a not for loan value > 0
1256 Need to check the issuingrules onshelfholds column,
1257 if this is set items on the shelf can be placed on hold
1259 Note that IsAvailableForItemLevelRequest() does not
1260 check if the staff operator is authorized to place
1261 a request on the item - in particular,
1262 this routine does not check IndependentBranches
1263 and canreservefromotherbranches.
1265 Note also that this subroutine does not checks smart
1266 rules limits for item by reservesallowed/holds_per_record
1267 values, this complemented in calling code with calls and
1268 checks with CanItemBeReserved or CanBookBeReserved.
1272 sub IsAvailableForItemLevelRequest {
1275 my $pickup_branchcode = shift;
1276 # items_any_available is precalculated status passed from request.pl when set of items
1277 # looped outside of IsAvailableForItemLevelRequest to avoid nested loops:
1278 my $items_any_available = shift;
1280 my $dbh = C4::Context->dbh;
1281 # must check the notforloan setting of the itemtype
1282 # FIXME - a lot of places in the code do this
1283 # or something similar - need to be
1285 my $itemtype = $item->effective_itemtype;
1286 my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1289 $notforloan_per_itemtype ||
1291 $item->notforloan > 0 ||
1293 ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1295 if ($pickup_branchcode) {
1296 my $destination = Koha::Libraries->find($pickup_branchcode);
1297 return 0 unless $destination;
1298 return 0 unless $destination->pickup_location;
1299 return 0 unless $item->can_be_transferred( { to => $destination } );
1300 my $reserves_control_branch =
1301 GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
1302 my $branchitemrule =
1303 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
1304 my $home_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
1305 return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
1308 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1310 if ( $on_shelf_holds == 1 ) {
1312 } elsif ( $on_shelf_holds == 2 ) {
1314 # if we have this param predefined from outer caller sub, we just need
1315 # to return it, so we saving from having loop inside other loop:
1316 return $items_any_available ? 0 : 1
1317 if defined $items_any_available;
1319 my $any_available = ItemsAnyAvailableAndNotRestricted( { biblionumber => $item->biblionumber, patron => $patron });
1320 return $any_available ? 0 : 1;
1321 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1322 return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1326 =head2 ItemsAnyAvailableAndNotRestricted
1328 ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron });
1330 This function checks all items for specified biblionumber (numeric) against patron (object)
1331 and returns true (1) if at least one item available for loan/check out/present/not held
1332 and also checks other parameters logic which not restricts item for hold at all (for ex.
1333 AllowHoldsOnDamagedItems or 'holdallowed' own/sibling library)
1337 sub ItemsAnyAvailableAndNotRestricted {
1340 my @items = Koha::Items->search( { biblionumber => $param->{biblionumber} } );
1342 foreach my $i (@items) {
1343 my $reserves_control_branch =
1344 GetReservesControlBranch( $i->unblessed(), $param->{patron}->unblessed );
1345 my $branchitemrule =
1346 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1347 my $item_library = Koha::Libraries->find( { branchcode => $i->homebranch } );
1349 # we can return (end the loop) when first one found:
1352 || $i->notforloan > 0
1355 || IsItemOnHoldAndFound( $i->id )
1357 && ! C4::Context->preference('AllowHoldsOnDamagedItems') )
1358 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1359 || $branchitemrule->{holdallowed} == 1 && $param->{patron}->branchcode ne $i->homebranch
1360 || $branchitemrule->{holdallowed} == 3 && ! $item_library->validate_hold_sibling( { branchcode => $param->{patron}->branchcode } )
1361 || CanItemBeReserved( $param->{patron}->borrowernumber, $i->id )->{status} ne 'OK';
1367 =head2 AlterPriority
1369 AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1371 This function changes a reserve's priority up, down, to the top, or to the bottom.
1372 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1377 my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1379 my $hold = Koha::Holds->find( $reserve_id );
1380 return unless $hold;
1382 if ( $hold->cancellationdate ) {
1383 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1387 if ( $where eq 'up' ) {
1388 return unless $prev_priority;
1389 _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1390 } elsif ( $where eq 'down' ) {
1391 return unless $next_priority;
1392 _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1393 } elsif ( $where eq 'top' ) {
1394 _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1395 } elsif ( $where eq 'bottom' ) {
1396 _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1399 # FIXME Should return the new priority
1402 =head2 ToggleLowestPriority
1404 ToggleLowestPriority( $borrowernumber, $biblionumber );
1406 This function sets the lowestPriority field to true if is false, and false if it is true.
1410 sub ToggleLowestPriority {
1411 my ( $reserve_id ) = @_;
1413 my $dbh = C4::Context->dbh;
1415 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1416 $sth->execute( $reserve_id );
1418 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1421 =head2 ToggleSuspend
1423 ToggleSuspend( $reserve_id );
1425 This function sets the suspend field to true if is false, and false if it is true.
1426 If the reserve is currently suspended with a suspend_until date, that date will
1427 be cleared when it is unsuspended.
1432 my ( $reserve_id, $suspend_until ) = @_;
1434 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1436 my $hold = Koha::Holds->find( $reserve_id );
1438 if ( $hold->is_suspended ) {
1441 $hold->suspend_hold( $suspend_until );
1448 borrowernumber => $borrowernumber,
1449 [ biblionumber => $biblionumber, ]
1450 [ suspend_until => $suspend_until, ]
1451 [ suspend => $suspend ]
1454 This function accepts a set of hash keys as its parameters.
1455 It requires either borrowernumber or biblionumber, or both.
1457 suspend_until is wholly optional.
1464 my $borrowernumber = $params{'borrowernumber'} || undef;
1465 my $biblionumber = $params{'biblionumber'} || undef;
1466 my $suspend_until = $params{'suspend_until'} || undef;
1467 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1469 $suspend_until = eval { dt_from_string($suspend_until) }
1470 if ( defined($suspend_until) );
1472 return unless ( $borrowernumber || $biblionumber );
1475 $params->{found} = undef;
1476 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1477 $params->{biblionumber} = $biblionumber if $biblionumber;
1479 my @holds = Koha::Holds->search($params);
1482 map { $_->suspend_hold($suspend_until) } @holds;
1485 map { $_->resume() } @holds;
1493 reserve_id => $reserve_id,
1495 [ignoreSetLowestRank => $ignoreSetLowestRank]
1500 _FixPriority({ biblionumber => $biblionumber});
1502 This routine adjusts the priority of a hold request and holds
1505 In the first form, where a reserve_id is passed, the priority of the
1506 hold is set to supplied rank, and other holds for that bib are adjusted
1507 accordingly. If the rank is "del", the hold is cancelled. If no rank
1508 is supplied, all of the holds on that bib have their priority adjusted
1509 as if the second form had been used.
1511 In the second form, where a biblionumber is passed, the holds on that
1512 bib (that are not captured) are sorted in order of increasing priority,
1513 then have reserves.priority set so that the first non-captured hold
1514 has its priority set to 1, the second non-captured hold has its priority
1515 set to 2, and so forth.
1517 In both cases, holds that have the lowestPriority flag on are have their
1518 priority adjusted to ensure that they remain at the end of the line.
1520 Note that the ignoreSetLowestRank parameter is meant to be used only
1521 when _FixPriority calls itself.
1526 my ( $params ) = @_;
1527 my $reserve_id = $params->{reserve_id};
1528 my $rank = $params->{rank} // '';
1529 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1530 my $biblionumber = $params->{biblionumber};
1532 my $dbh = C4::Context->dbh;
1535 if ( $reserve_id ) {
1536 $hold = Koha::Holds->find( $reserve_id );
1537 if (!defined $hold){
1538 # may have already been checked out and hold fulfilled
1539 $hold = Koha::Old::Holds->find( $reserve_id );
1541 return unless $hold;
1544 unless ( $biblionumber ) { # FIXME This is a very weird API
1545 $biblionumber = $hold->biblionumber;
1548 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1551 elsif ( $reserve_id && ( $rank eq "W" || $rank eq "0" ) ) {
1553 # make sure priority for waiting or in-transit items is 0
1557 WHERE reserve_id = ?
1558 AND found IN ('W', 'T')
1560 my $sth = $dbh->prepare($query);
1561 $sth->execute( $reserve_id );
1567 SELECT reserve_id, borrowernumber, reservedate
1569 WHERE biblionumber = ?
1570 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1571 ORDER BY priority ASC
1573 my $sth = $dbh->prepare($query);
1574 $sth->execute( $biblionumber );
1575 while ( my $line = $sth->fetchrow_hashref ) {
1576 push( @priority, $line );
1579 # FIXME This whole sub must be rewritten, especially to highlight what is done when reserve_id is not given
1580 # To find the matching index
1582 my $key = -1; # to allow for 0 to be a valid result
1583 for ( $i = 0 ; $i < @priority ; $i++ ) {
1584 if ( $reserve_id && $reserve_id == $priority[$i]->{'reserve_id'} ) {
1585 $key = $i; # save the index
1590 # if index exists in array then move it to new position
1591 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1592 my $new_rank = $rank -
1593 1; # $new_rank is what you want the new index to be in the array
1594 my $moving_item = splice( @priority, $key, 1 );
1595 splice( @priority, $new_rank, 0, $moving_item );
1598 # now fix the priority on those that are left....
1602 WHERE reserve_id = ?
1604 $sth = $dbh->prepare($query);
1605 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1608 $priority[$j]->{'reserve_id'}
1612 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1615 unless ( $ignoreSetLowestRank ) {
1616 while ( my $res = $sth->fetchrow_hashref() ) {
1618 reserve_id => $res->{'reserve_id'},
1620 ignoreSetLowestRank => 1
1626 =head2 _Findgroupreserve
1628 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1630 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1631 first match found. If neither, then we look for non-holds-queue based holds.
1632 Lookahead is the number of days to look in advance.
1634 C<&_Findgroupreserve> returns :
1635 C<@results> is an array of references-to-hash whose keys are mostly
1636 fields from the reserves table of the Koha database, plus
1637 C<biblioitemnumber>.
1639 This routine with either return:
1640 1 - Item specific holds from the holds queue
1641 2 - Title level holds from the holds queue
1642 3 - All holds for this biblionumber
1644 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1648 sub _Findgroupreserve {
1649 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1650 my $dbh = C4::Context->dbh;
1652 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1653 # check for exact targeted match
1654 my $item_level_target_query = qq{
1655 SELECT reserves.biblionumber AS biblionumber,
1656 reserves.borrowernumber AS borrowernumber,
1657 reserves.reservedate AS reservedate,
1658 reserves.branchcode AS branchcode,
1659 reserves.cancellationdate AS cancellationdate,
1660 reserves.found AS found,
1661 reserves.reservenotes AS reservenotes,
1662 reserves.priority AS priority,
1663 reserves.timestamp AS timestamp,
1664 biblioitems.biblioitemnumber AS biblioitemnumber,
1665 reserves.itemnumber AS itemnumber,
1666 reserves.reserve_id AS reserve_id,
1667 reserves.itemtype AS itemtype
1669 JOIN biblioitems USING (biblionumber)
1670 JOIN hold_fill_targets USING (reserve_id)
1673 AND item_level_request = 1
1674 AND hold_fill_targets.itemnumber = ?
1675 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1679 my $sth = $dbh->prepare($item_level_target_query);
1680 $sth->execute($itemnumber, $lookahead||0);
1682 if ( my $data = $sth->fetchrow_hashref ) {
1683 push( @results, $data )
1684 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1686 return @results if @results;
1688 # check for title-level targeted match
1689 my $title_level_target_query = qq{
1690 SELECT reserves.biblionumber AS biblionumber,
1691 reserves.borrowernumber AS borrowernumber,
1692 reserves.reservedate AS reservedate,
1693 reserves.branchcode AS branchcode,
1694 reserves.cancellationdate AS cancellationdate,
1695 reserves.found AS found,
1696 reserves.reservenotes AS reservenotes,
1697 reserves.priority AS priority,
1698 reserves.timestamp AS timestamp,
1699 biblioitems.biblioitemnumber AS biblioitemnumber,
1700 reserves.itemnumber AS itemnumber,
1701 reserves.reserve_id AS reserve_id,
1702 reserves.itemtype AS itemtype
1704 JOIN biblioitems USING (biblionumber)
1705 JOIN hold_fill_targets USING (reserve_id)
1708 AND item_level_request = 0
1709 AND hold_fill_targets.itemnumber = ?
1710 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1714 $sth = $dbh->prepare($title_level_target_query);
1715 $sth->execute($itemnumber, $lookahead||0);
1717 if ( my $data = $sth->fetchrow_hashref ) {
1718 push( @results, $data )
1719 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1721 return @results if @results;
1724 SELECT reserves.biblionumber AS biblionumber,
1725 reserves.borrowernumber AS borrowernumber,
1726 reserves.reservedate AS reservedate,
1727 reserves.waitingdate AS waitingdate,
1728 reserves.branchcode AS branchcode,
1729 reserves.cancellationdate AS cancellationdate,
1730 reserves.found AS found,
1731 reserves.reservenotes AS reservenotes,
1732 reserves.priority AS priority,
1733 reserves.timestamp AS timestamp,
1734 reserves.itemnumber AS itemnumber,
1735 reserves.reserve_id AS reserve_id,
1736 reserves.itemtype AS itemtype
1738 WHERE reserves.biblionumber = ?
1739 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1740 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1744 $sth = $dbh->prepare($query);
1745 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1747 while ( my $data = $sth->fetchrow_hashref ) {
1748 push( @results, $data )
1749 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1754 =head2 _koha_notify_reserve
1756 _koha_notify_reserve( $hold->reserve_id );
1758 Sends a notification to the patron that their hold has been filled (through
1759 ModReserveAffect, _not_ ModReserveFill)
1761 The letter code for this notice may be found using the following query:
1763 select distinct letter_code
1764 from message_transports
1765 inner join message_attributes using (message_attribute_id)
1766 where message_name = 'Hold_Filled'
1768 This will probably sipmly be 'HOLD', but because it is defined in the database,
1769 it is subject to addition or change.
1771 The following tables are availalbe witin the notice:
1782 sub _koha_notify_reserve {
1783 my $reserve_id = shift;
1784 my $hold = Koha::Holds->find($reserve_id);
1785 my $borrowernumber = $hold->borrowernumber;
1787 my $patron = Koha::Patrons->find( $borrowernumber );
1789 # Try to get the borrower's email address
1790 my $to_address = $patron->notice_email_address;
1792 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1793 borrowernumber => $borrowernumber,
1794 message_name => 'Hold_Filled'
1797 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1799 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1801 my %letter_params = (
1802 module => 'reserves',
1803 branchcode => $hold->branchcode,
1804 lang => $patron->lang,
1806 'branches' => $library,
1807 'borrowers' => $patron->unblessed,
1808 'biblio' => $hold->biblionumber,
1809 'biblioitems' => $hold->biblionumber,
1810 'reserves' => $hold->unblessed,
1811 'items' => $hold->itemnumber,
1815 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.
1816 my $send_notification = sub {
1817 my ( $mtt, $letter_code ) = (@_);
1818 return unless defined $letter_code;
1819 $letter_params{letter_code} = $letter_code;
1820 $letter_params{message_transport_type} = $mtt;
1821 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1823 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1827 C4::Letters::EnqueueLetter( {
1829 borrowernumber => $borrowernumber,
1830 from_address => $admin_email_address,
1831 message_transport_type => $mtt,
1835 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1837 ( $mtt eq 'email' and not $to_address ) # No email address
1838 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1839 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1842 &$send_notification($mtt, $letter_code);
1843 $notification_sent++;
1845 #Making sure that a print notification is sent if no other transport types can be utilized.
1846 if (! $notification_sent) {
1847 &$send_notification('print', 'HOLD');
1852 =head2 _ShiftPriorityByDateAndPriority
1854 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1856 This increments the priority of all reserves after the one
1857 with either the lowest date after C<$reservedate>
1858 or the lowest priority after C<$priority>.
1860 It effectively makes room for a new reserve to be inserted with a certain
1861 priority, which is returned.
1863 This is most useful when the reservedate can be set by the user. It allows
1864 the new reserve to be placed before other reserves that have a later
1865 reservedate. Since priority also is set by the form in reserves/request.pl
1866 the sub accounts for that too.
1870 sub _ShiftPriorityByDateAndPriority {
1871 my ( $biblio, $resdate, $new_priority ) = @_;
1873 my $dbh = C4::Context->dbh;
1874 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1875 my $sth = $dbh->prepare( $query );
1876 $sth->execute( $biblio, $resdate, $new_priority );
1877 my $min_priority = $sth->fetchrow;
1878 # if no such matches are found, $new_priority remains as original value
1879 $new_priority = $min_priority if ( $min_priority );
1881 # Shift the priority up by one; works in conjunction with the next SQL statement
1882 $query = "UPDATE reserves
1883 SET priority = priority+1
1884 WHERE biblionumber = ?
1885 AND borrowernumber = ?
1888 my $sth_update = $dbh->prepare( $query );
1890 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1891 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1892 $sth = $dbh->prepare( $query );
1893 $sth->execute( $new_priority, $biblio );
1894 while ( my $row = $sth->fetchrow_hashref ) {
1895 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1898 return $new_priority; # so the caller knows what priority they wind up receiving
1903 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1905 Use when checking out an item to handle reserves
1906 If $cancelreserve boolean is set to true, it will remove existing reserve
1911 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1913 $cancelreserve //= 0;
1915 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1916 my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1919 my $biblionumber = $res->{biblionumber};
1921 if ($res->{borrowernumber} == $borrowernumber) {
1922 ModReserveFill($res);
1926 # The item is reserved by someone else.
1927 # Find this item in the reserves
1929 my $borr_res = Koha::Holds->search({
1930 borrowernumber => $borrowernumber,
1931 biblionumber => $biblionumber,
1933 order_by => 'priority'
1937 # The item is reserved by the current patron
1938 ModReserveFill($borr_res->unblessed);
1941 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1942 RevertWaitingStatus({ itemnumber => $itemnumber });
1944 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1945 my $hold = Koha::Holds->find( $res->{reserve_id} );
1953 MergeHolds($dbh,$to_biblio, $from_biblio);
1955 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1960 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1961 my $sth = $dbh->prepare(
1962 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1964 $sth->execute($from_biblio);
1965 if ( my $data = $sth->fetchrow_hashref() ) {
1967 # holds exist on old record, if not we don't need to do anything
1968 $sth = $dbh->prepare(
1969 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1970 $sth->execute( $to_biblio, $from_biblio );
1973 # don't reorder those already waiting
1975 $sth = $dbh->prepare(
1976 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1978 my $upd_sth = $dbh->prepare(
1979 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1980 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1982 $sth->execute( $to_biblio, 'W', 'T' );
1984 while ( my $reserve = $sth->fetchrow_hashref() ) {
1986 $priority, $to_biblio,
1987 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1988 $reserve->{'itemnumber'}
1995 =head2 RevertWaitingStatus
1997 RevertWaitingStatus({ itemnumber => $itemnumber });
1999 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2001 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2002 item level hold, even if it was only a bibliolevel hold to
2003 begin with. This is because we can no longer know if a hold
2004 was item-level or bib-level after a hold has been set to
2009 sub RevertWaitingStatus {
2010 my ( $params ) = @_;
2011 my $itemnumber = $params->{'itemnumber'};
2013 return unless ( $itemnumber );
2015 my $dbh = C4::Context->dbh;
2017 ## Get the waiting reserve we want to revert
2018 my $hold = Koha::Holds->search(
2020 itemnumber => $itemnumber,
2021 found => { not => undef },
2025 ## Increment the priority of all other non-waiting
2026 ## reserves for this bib record
2027 my $holds = Koha::Holds->search({ biblionumber => $hold->biblionumber, priority => { '>' => 0 } })
2028 ->update({ priority => \'priority + 1' }, { no_triggers => 1 });
2030 ## Fix up the currently waiting reserve
2035 waitingdate => undef,
2036 itemnumber => $hold->item_level_hold ? $hold->itemnumber : undef,
2040 _FixPriority( { biblionumber => $hold->biblionumber } );
2049 branchcode => $branchcode,
2050 borrowernumber => $borrowernumber,
2051 biblionumber => $biblionumber,
2052 [ itemnumber => $itemnumber, ]
2053 [ barcode => $barcode, ]
2057 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2059 The letter code will be HOLD_SLIP, and the following tables are
2060 available within the slip:
2073 my $branchcode = $args->{branchcode};
2074 my $reserve_id = $args->{reserve_id};
2076 my $hold = Koha::Holds->find($reserve_id);
2077 return unless $hold;
2079 my $patron = $hold->borrower;
2080 my $reserve = $hold->unblessed;
2082 return C4::Letters::GetPreparedLetter (
2083 module => 'circulation',
2084 letter_code => 'HOLD_SLIP',
2085 branchcode => $branchcode,
2086 lang => $patron->lang,
2088 'reserves' => $reserve,
2089 'branches' => $reserve->{branchcode},
2090 'borrowers' => $reserve->{borrowernumber},
2091 'biblio' => $reserve->{biblionumber},
2092 'biblioitems' => $reserve->{biblionumber},
2093 'items' => $reserve->{itemnumber},
2098 =head2 GetReservesControlBranch
2100 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2102 Return the branchcode to be used to determine which reserves
2103 policy applies to a transaction.
2105 C<$item> is a hashref for an item. Only 'homebranch' is used.
2107 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2111 sub GetReservesControlBranch {
2112 my ( $item, $borrower ) = @_;
2114 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2117 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2118 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2124 =head2 CalculatePriority
2126 my $p = CalculatePriority($biblionumber, $resdate);
2128 Calculate priority for a new reserve on biblionumber, placing it at
2129 the end of the line of all holds whose start date falls before
2130 the current system time and that are neither on the hold shelf
2133 The reserve date parameter is optional; if it is supplied, the
2134 priority is based on the set of holds whose start date falls before
2135 the parameter value.
2137 After calculation of this priority, it is recommended to call
2138 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2143 sub CalculatePriority {
2144 my ( $biblionumber, $resdate ) = @_;
2147 SELECT COUNT(*) FROM reserves
2148 WHERE biblionumber = ?
2150 AND (found IS NULL OR found = '')
2152 #skip found==W or found==T (waiting or transit holds)
2154 $sql.= ' AND ( reservedate <= ? )';
2157 $sql.= ' AND ( reservedate < NOW() )';
2159 my $dbh = C4::Context->dbh();
2160 my @row = $dbh->selectrow_array(
2163 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2166 return @row ? $row[0]+1 : 1;
2169 =head2 IsItemOnHoldAndFound
2171 my $bool = IsItemFoundHold( $itemnumber );
2173 Returns true if the item is currently on hold
2174 and that hold has a non-null found status ( W, T, etc. )
2178 sub IsItemOnHoldAndFound {
2179 my ($itemnumber) = @_;
2181 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2183 my $found = $rs->count(
2185 itemnumber => $itemnumber,
2186 found => { '!=' => undef }
2193 =head2 GetMaxPatronHoldsForRecord
2195 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2197 For multiple holds on a given record for a given patron, the max
2198 number of record level holds that a patron can be placed is the highest
2199 value of the holds_per_record rule for each item if the record for that
2200 patron. This subroutine finds and returns the highest holds_per_record
2201 rule value for a given patron id and record id.
2205 sub GetMaxPatronHoldsForRecord {
2206 my ( $borrowernumber, $biblionumber ) = @_;
2208 my $patron = Koha::Patrons->find($borrowernumber);
2209 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2211 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2213 my $categorycode = $patron->categorycode;
2215 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2218 foreach my $item (@items) {
2219 my $itemtype = $item->effective_itemtype();
2221 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2223 my $rule = Koha::CirculationRules->get_effective_rule({
2224 categorycode => $categorycode,
2225 itemtype => $itemtype,
2226 branchcode => $branchcode,
2227 rule_name => 'holds_per_record'
2229 my $holds_per_record = $rule ? $rule->rule_value : 0;
2230 $max = $holds_per_record if $holds_per_record > $max;
2238 Koha Development Team <http://koha-community.org/>