3 # Copyright 2000-2002 Katipo Communications
4 # 2006 SAN Ouest Provence
5 # 2007-2010 BibLibre Paul POULAIN
8 # This file is part of Koha.
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
25 #use warnings; FIXME - Bug 2505
33 # for _koha_notify_reserve
34 use C4::Members::Messaging;
47 use Koha::IssuingRules;
52 use List::MoreUtils qw( firstidx any );
56 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
60 C4::Reserves - Koha functions for dealing with reservation.
68 This modules provides somes functions to deal with reservations.
70 Reserves are stored in reserves table.
71 The following columns contains important values :
72 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
73 =0 : then the reserve is being dealed
74 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
75 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
76 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
77 F(inished) : the reserve has been completed, and is done
78 - itemnumber : empty : the reserve is still unaffected to an item
79 filled: the reserve is attached to an item
80 The complete workflow is :
81 ==== 1st use case ====
82 patron request a document, 1st available : P >0, F=NULL, I=NULL
83 a library having it run "transfertodo", and clic on the list
84 if there is no transfer to do, the reserve waiting
85 patron can pick it up P =0, F=W, I=filled
86 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
87 The pickup library receive the book, it check in P =0, F=W, I=filled
88 The patron borrow the book P =0, F=F, I=filled
90 ==== 2nd use case ====
91 patron requests a document, a given item,
92 If pickup is holding branch P =0, F=W, I=filled
93 If transfer needed, write in branchtransfer P =0, F=T, I=filled
94 The pickup library receive the book, it checks it in P =0, F=W, I=filled
95 The patron borrow the book P =0, F=F, I=filled
116 &ModReserveMinusPriority
122 &CanReserveBeCanceledFromOpac
123 &CancelExpiredReserves
125 &AutoUnsuspendReserves
127 &IsAvailableForItemLevelRequest
130 &ToggleLowestPriority
136 &GetReservesControlBranch
140 GetMaxPatronHoldsForRecord
142 @EXPORT_OK = qw( MergeHolds );
147 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
149 Adds reserve and generates HOLDPLACED message.
151 The following tables are available witin the HOLDPLACED message:
164 $branch, $borrowernumber, $biblionumber, $bibitems,
165 $priority, $resdate, $expdate, $notes,
166 $title, $checkitem, $found, $itemtype
169 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
170 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
172 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
174 # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
175 # of the document, we force the value $priority and $found .
176 if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
178 my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
179 if ( $item->holdingbranch eq $branch ) {
184 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
186 # Make room in reserves for this before those of a later reserve date
187 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
192 # If the reserv had the waiting status, we had the value of the resdate
193 if ( $found eq 'W' ) {
194 $waitingdate = $resdate;
197 # Don't add itemtype limit if specific item is selected
198 $itemtype = undef if $checkitem;
200 # updates take place here
201 my $hold = Koha::Hold->new(
203 borrowernumber => $borrowernumber,
204 biblionumber => $biblionumber,
205 reservedate => $resdate,
206 branchcode => $branch,
207 priority => $priority,
208 reservenotes => $notes,
209 itemnumber => $checkitem,
211 waitingdate => $waitingdate,
212 expirationdate => $expdate,
213 itemtype => $itemtype,
216 $hold->set_waiting() if $found eq 'W';
218 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
219 if C4::Context->preference('HoldsLog');
221 my $reserve_id = $hold->id();
223 # add a reserve fee if needed
224 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
225 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
226 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
229 _FixPriority({ biblionumber => $biblionumber});
231 # Send e-mail to librarian if syspref is active
232 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
233 my $patron = Koha::Patrons->find( $borrowernumber );
234 my $library = $patron->library;
235 if ( my $letter = C4::Letters::GetPreparedLetter (
236 module => 'reserves',
237 letter_code => 'HOLDPLACED',
238 branchcode => $branch,
239 lang => $patron->lang,
241 'branches' => $library->unblessed,
242 'borrowers' => $patron->unblessed,
243 'biblio' => $biblionumber,
244 'biblioitems' => $biblionumber,
245 'items' => $checkitem,
246 'reserves' => $hold->unblessed,
250 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
252 C4::Letters::EnqueueLetter(
254 borrowernumber => $borrowernumber,
255 message_transport_type => 'email',
256 from_address => $admin_email_address,
257 to_address => $admin_email_address,
266 =head2 CanBookBeReserved
268 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber)
269 if ($canReserve eq 'OK') { #We can reserve this Item! }
271 See CanItemBeReserved() for possible return values.
275 sub CanBookBeReserved{
276 my ($borrowernumber, $biblionumber) = @_;
278 my $items = GetItemnumbersForBiblio($biblionumber);
279 #get items linked via host records
280 my @hostitems = get_hostitemnumbers_of($biblionumber);
282 push (@$items,@hostitems);
286 foreach my $item (@$items) {
287 $canReserve = CanItemBeReserved( $borrowernumber, $item );
288 return $canReserve if $canReserve->{status} eq 'OK';
293 =head2 CanItemBeReserved
295 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber)
296 if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
298 @RETURNS { status => OK }, if the Item can be reserved.
299 { status => ageRestricted }, if the Item is age restricted for this borrower.
300 { status => damaged }, if the Item is damaged.
301 { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
302 { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
303 { status => notReservable }, if holds on this item are not allowed
307 sub CanItemBeReserved {
308 my ( $borrowernumber, $itemnumber ) = @_;
310 my $dbh = C4::Context->dbh;
311 my $ruleitemtype; # itemtype of the matching issuing rule
312 my $allowedreserves = 0; # Total number of holds allowed across all records
313 my $holds_per_record = 1; # Total number of holds allowed for this one given record
315 # we retrieve borrowers and items informations #
316 # item->{itype} will come for biblioitems if necessery
317 my $item = GetItem($itemnumber);
318 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
319 my $patron = Koha::Patrons->find( $borrowernumber );
320 my $borrower = $patron->unblessed;
322 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
323 return { status =>'damaged' }
324 if ( $item->{damaged}
325 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
327 # Check for the age restriction
328 my ( $ageRestriction, $daysToAgeRestriction ) =
329 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
330 return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
332 # Check that the patron doesn't have an item level hold on this item already
333 return { status =>'itemAlreadyOnHold' }
334 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
336 my $controlbranch = C4::Context->preference('ReservesControlBranch');
339 SELECT count(*) AS count
341 LEFT JOIN items USING (itemnumber)
342 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
343 LEFT JOIN borrowers USING (borrowernumber)
344 WHERE borrowernumber = ?
348 my $branchfield = "reserves.branchcode";
350 if ( $controlbranch eq "ItemHomeLibrary" ) {
351 $branchfield = "items.homebranch";
352 $branchcode = $item->{homebranch};
354 elsif ( $controlbranch eq "PatronLibrary" ) {
355 $branchfield = "borrowers.branchcode";
356 $branchcode = $borrower->{branchcode};
360 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) {
361 $ruleitemtype = $rights->{itemtype};
362 $allowedreserves = $rights->{reservesallowed};
363 $holds_per_record = $rights->{holds_per_record};
369 $item = Koha::Items->find( $itemnumber );
370 my $holds = Koha::Holds->search(
372 borrowernumber => $borrowernumber,
373 biblionumber => $item->biblionumber,
374 found => undef, # Found holds don't count against a patron's holds limit
377 if ( $holds->count() >= $holds_per_record ) {
378 return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
383 $querycount .= "AND $branchfield = ?";
385 # If using item-level itypes, fall back to the record
386 # level itemtype if the hold has no associated item
388 C4::Context->preference('item-level_itypes')
389 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
390 : " AND biblioitems.itemtype = ?"
391 if ( $ruleitemtype ne "*" );
393 my $sthcount = $dbh->prepare($querycount);
395 if ( $ruleitemtype eq "*" ) {
396 $sthcount->execute( $borrowernumber, $branchcode );
399 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
402 my $reservecount = "0";
403 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
404 $reservecount = $rowcount->{count};
407 # we check if it's ok or not
408 if ( $reservecount >= $allowedreserves ) {
409 return { status => 'tooManyReserves', limit => $allowedreserves };
412 # Now we need to check hold limits by patron category
413 my $schema = Koha::Database->new()->schema();
414 my $rule = $schema->resultset('BranchBorrowerCircRule')->find(
416 branchcode => $branchcode,
417 categorycode => $borrower->{categorycode},
420 $rule ||= $schema->resultset('DefaultBorrowerCircRule')->find(
422 categorycode => $borrower->{categorycode}
425 if ( $rule && defined $rule->max_holds ) {
426 my $total_holds_count = Koha::Holds->search(
428 borrowernumber => $borrower->{borrowernumber}
432 return { status => 'tooManyReserves', limit => $rule->max_holds } if $total_holds_count >= $rule->max_holds;
435 my $circ_control_branch =
436 C4::Circulation::_GetCircControlBranch( $item->unblessed(), $borrower );
438 C4::Circulation::GetBranchItemRule( $circ_control_branch, $item->itype );
440 if ( $branchitemrule->{holdallowed} == 0 ) {
441 return { status => 'notReservable' };
444 if ( $branchitemrule->{holdallowed} == 1
445 && $borrower->{branchcode} ne $item->homebranch )
447 return { status => 'cannotReserveFromOtherBranches' };
450 # If reservecount is ok, we check item branch if IndependentBranches is ON
451 # and canreservefromotherbranches is OFF
452 if ( C4::Context->preference('IndependentBranches')
453 and !C4::Context->preference('canreservefromotherbranches') )
455 my $itembranch = $item->homebranch;
456 if ( $itembranch ne $borrower->{branchcode} ) {
457 return { status => 'cannotReserveFromOtherBranches' };
461 return { status => 'OK' };
464 =head2 CanReserveBeCanceledFromOpac
466 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
468 returns 1 if reserve can be cancelled by user from OPAC.
469 First check if reserve belongs to user, next checks if reserve is not in
470 transfer or waiting status
474 sub CanReserveBeCanceledFromOpac {
475 my ($reserve_id, $borrowernumber) = @_;
477 return unless $reserve_id and $borrowernumber;
478 my $reserve = Koha::Holds->find($reserve_id);
480 return 0 unless $reserve->borrowernumber == $borrowernumber;
481 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
487 =head2 GetOtherReserves
489 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
491 Check queued list of this document and check if this document must be transferred
495 sub GetOtherReserves {
496 my ($itemnumber) = @_;
499 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
500 if ($checkreserves) {
501 my $iteminfo = GetItem($itemnumber);
502 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
503 $messages->{'transfert'} = $checkreserves->{'branchcode'};
504 #minus priorities of others reservs
505 ModReserveMinusPriority(
507 $checkreserves->{'reserve_id'},
510 #launch the subroutine dotransfer
511 C4::Items::ModItemTransfer(
513 $iteminfo->{'holdingbranch'},
514 $checkreserves->{'branchcode'}
519 #step 2b : case of a reservation on the same branch, set the waiting status
521 $messages->{'waiting'} = 1;
522 ModReserveMinusPriority(
524 $checkreserves->{'reserve_id'},
526 ModReserveStatus($itemnumber,'W');
529 $nextreservinfo = $checkreserves->{'borrowernumber'};
532 return ( $messages, $nextreservinfo );
535 =head2 ChargeReserveFee
537 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
539 Charge the fee for a reserve (if $fee > 0)
543 sub ChargeReserveFee {
544 my ( $borrowernumber, $fee, $title ) = @_;
545 return if !$fee || $fee==0; # the last test is needed to include 0.00
547 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
549 my $dbh = C4::Context->dbh;
550 my $nextacctno = &getnextacctno( $borrowernumber );
551 $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
556 $fee = GetReserveFee( $borrowernumber, $biblionumber );
558 Calculate the fee for a reserve (if applicable).
563 my ( $borrowernumber, $biblionumber ) = @_;
565 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
568 SELECT COUNT(*) FROM items
569 LEFT JOIN issues USING (itemnumber)
570 WHERE items.biblionumber=? AND issues.issue_id IS NULL
573 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
576 my $dbh = C4::Context->dbh;
577 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
578 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
579 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
580 # This is a reconstruction of the old code:
581 # Compare number of items with items issued, and optionally check holds
582 # If not all items are issued and there are no holds: charge no fee
583 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
584 my ( $notissued, $reserved );
585 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
588 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
589 ( $biblionumber, $borrowernumber ) );
590 $fee = 0 if $reserved == 0;
596 =head2 GetReserveStatus
598 $reservestatus = GetReserveStatus($itemnumber);
600 Takes an itemnumber and returns the status of the reserve placed on it.
601 If several reserves exist, the reserve with the lower priority is given.
605 ## FIXME: I don't think this does what it thinks it does.
606 ## It only ever checks the first reserve result, even though
607 ## multiple reserves for that bib can have the itemnumber set
608 ## the sub is only used once in the codebase.
609 sub GetReserveStatus {
610 my ($itemnumber) = @_;
612 my $dbh = C4::Context->dbh;
614 my ($sth, $found, $priority);
616 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
617 $sth->execute($itemnumber);
618 ($found, $priority) = $sth->fetchrow_array;
622 return 'Waiting' if $found eq 'W' and $priority == 0;
623 return 'Finished' if $found eq 'F';
626 return 'Reserved' if $priority > 0;
628 return ''; # empty string here will remove need for checking undef, or less log lines
633 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
634 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
635 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
637 Find a book in the reserves.
639 C<$itemnumber> is the book's item number.
640 C<$lookahead> is the number of days to look in advance for future reserves.
642 As I understand it, C<&CheckReserves> looks for the given item in the
643 reserves. If it is found, that's a match, and C<$status> is set to
646 Otherwise, it finds the most important item in the reserves with the
647 same biblio number as this book (I'm not clear on this) and returns it
648 with C<$status> set to C<Reserved>.
650 C<&CheckReserves> returns a two-element list:
652 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
654 C<$reserve> is the reserve item that matched. It is a
655 reference-to-hash whose keys are mostly the fields of the reserves
656 table in the Koha database.
661 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
662 my $dbh = C4::Context->dbh;
665 if (C4::Context->preference('item-level_itypes')){
667 SELECT items.biblionumber,
668 items.biblioitemnumber,
669 itemtypes.notforloan,
670 items.notforloan AS itemnotforloan,
676 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
677 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
682 SELECT items.biblionumber,
683 items.biblioitemnumber,
684 itemtypes.notforloan,
685 items.notforloan AS itemnotforloan,
691 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
692 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
697 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
698 $sth->execute($item);
701 $sth = $dbh->prepare("$select WHERE barcode = ?");
702 $sth->execute($barcode);
704 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
705 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
707 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
709 return unless $itemnumber; # bail if we got nothing.
711 # if item is not for loan it cannot be reserved either.....
712 # except where items.notforloan < 0 : This indicates the item is holdable.
713 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
715 # Find this item in the reserves
716 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
718 # $priority and $highest are used to find the most important item
719 # in the list returned by &_Findgroupreserve. (The lower $priority,
720 # the more important the item.)
721 # $highest is the most important item we've seen so far.
723 if (scalar @reserves) {
724 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
725 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
726 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
728 my $priority = 10000000;
729 foreach my $res (@reserves) {
730 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
731 if ($res->{'found'} eq 'W') {
732 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
734 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
739 my $local_hold_match;
741 if ($LocalHoldsPriority) {
742 $patron = Koha::Patrons->find( $res->{borrowernumber} );
743 $iteminfo = C4::Items::GetItem($itemnumber);
745 my $local_holds_priority_item_branchcode =
746 $iteminfo->{$LocalHoldsPriorityItemControl};
747 my $local_holds_priority_patron_branchcode =
748 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
750 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
751 ? $patron->branchcode
754 $local_holds_priority_item_branchcode eq
755 $local_holds_priority_patron_branchcode;
758 # See if this item is more important than what we've got so far
759 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
760 $iteminfo ||= C4::Items::GetItem($itemnumber);
761 next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
762 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
763 my $branch = GetReservesControlBranch( $iteminfo, $patron->unblessed );
764 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
765 next if ($branchitemrule->{'holdallowed'} == 0);
766 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
767 next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
768 $priority = $res->{'priority'};
770 last if $local_hold_match;
776 # If we get this far, then no exact match was found.
777 # We return the most important (i.e. next) reservation.
779 $highest->{'itemnumber'} = $item;
780 return ( "Reserved", $highest, \@reserves );
786 =head2 CancelExpiredReserves
788 CancelExpiredReserves();
790 Cancels all reserves with an expiration date from before today.
794 sub CancelExpiredReserves {
795 my $today = dt_from_string();
796 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
797 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
799 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
800 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
801 $params->{found} = undef unless $expireWaiting;
803 # FIXME To move to Koha::Holds->search_expired (?)
804 my $holds = Koha::Holds->search( $params );
806 while ( my $hold = $holds->next ) {
807 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
809 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
811 my $cancel_params = {};
812 if ( $hold->found eq 'W' ) {
813 $cancel_params->{charge_cancel_fee} = 1;
815 $hold->cancel( $cancel_params );
819 =head2 AutoUnsuspendReserves
821 AutoUnsuspendReserves();
823 Unsuspends all suspended reserves with a suspend_until date from before today.
827 sub AutoUnsuspendReserves {
828 my $today = dt_from_string();
830 my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
832 map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
837 ModReserve({ rank => $rank,
838 reserve_id => $reserve_id,
839 branchcode => $branchcode
840 [, itemnumber => $itemnumber ]
841 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
844 Change a hold request's priority or cancel it.
846 C<$rank> specifies the effect of the change. If C<$rank>
847 is 'W' or 'n', nothing happens. This corresponds to leaving a
848 request alone when changing its priority in the holds queue
851 If C<$rank> is 'del', the hold request is cancelled.
853 If C<$rank> is an integer greater than zero, the priority of
854 the request is set to that value. Since priority != 0 means
855 that the item is not waiting on the hold shelf, setting the
856 priority to a non-zero value also sets the request's found
857 status and waiting date to NULL.
859 The optional C<$itemnumber> parameter is used only when
860 C<$rank> is a non-zero integer; if supplied, the itemnumber
861 of the hold request is set accordingly; if omitted, the itemnumber
864 B<FIXME:> Note that the forgoing can have the effect of causing
865 item-level hold requests to turn into title-level requests. This
866 will be fixed once reserves has separate columns for requested
867 itemnumber and supplying itemnumber.
874 my $rank = $params->{'rank'};
875 my $reserve_id = $params->{'reserve_id'};
876 my $branchcode = $params->{'branchcode'};
877 my $itemnumber = $params->{'itemnumber'};
878 my $suspend_until = $params->{'suspend_until'};
879 my $borrowernumber = $params->{'borrowernumber'};
880 my $biblionumber = $params->{'biblionumber'};
882 return if $rank eq "W";
883 return if $rank eq "n";
885 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
888 unless ( $reserve_id ) {
889 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
890 return unless $holds->count; # FIXME Should raise an exception
891 $hold = $holds->next;
892 $reserve_id = $hold->reserve_id;
895 $hold ||= Koha::Holds->find($reserve_id);
897 if ( $rank eq "del" ) {
900 elsif ($rank =~ /^\d+/ and $rank > 0) {
901 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
902 if C4::Context->preference('HoldsLog');
907 branchcode => $branchcode,
908 itemnumber => $itemnumber,
914 if ( defined( $suspend_until ) ) {
915 if ( $suspend_until ) {
916 $suspend_until = eval { dt_from_string( $suspend_until ) };
917 $hold->suspend_hold( $suspend_until );
919 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
920 # If the hold is not suspended, this does nothing.
921 $hold->set( { suspend_until => undef } )->store();
925 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
929 =head2 ModReserveFill
931 &ModReserveFill($reserve);
933 Fill a reserve. If I understand this correctly, this means that the
934 reserved book has been found and given to the patron who reserved it.
936 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
937 whose keys are fields from the reserves table in the Koha database.
943 my $reserve_id = $res->{'reserve_id'};
945 my $hold = Koha::Holds->find($reserve_id);
947 # get the priority on this record....
948 my $priority = $hold->priority;
950 # update the hold statuses, no need to store it though, we will be deleting it anyway
958 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
959 Koha::Old::Hold->new( $hold->unblessed() )->store();
963 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
964 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
965 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
968 # now fix the priority on the others (if the priority wasn't
969 # already sorted!)....
970 unless ( $priority == 0 ) {
971 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
975 =head2 ModReserveStatus
977 &ModReserveStatus($itemnumber, $newstatus);
979 Update the reserve status for the active (priority=0) reserve.
981 $itemnumber is the itemnumber the reserve is on
983 $newstatus is the new status.
987 sub ModReserveStatus {
989 #first : check if we have a reservation for this item .
990 my ($itemnumber, $newstatus) = @_;
991 my $dbh = C4::Context->dbh;
993 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
994 my $sth_set = $dbh->prepare($query);
995 $sth_set->execute( $newstatus, $itemnumber );
997 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
998 CartToShelf( $itemnumber );
1002 =head2 ModReserveAffect
1004 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1006 This function affect an item and a status for a given reserve, either fetched directly
1007 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1008 is given, only first reserve returned is affected, which is ok for anything but
1011 if $transferToDo is not set, then the status is set to "Waiting" as well.
1012 otherwise, a transfer is on the way, and the end of the transfer will
1013 take care of the waiting status
1017 sub ModReserveAffect {
1018 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1019 my $dbh = C4::Context->dbh;
1021 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1022 # attached to $itemnumber
1023 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1024 $sth->execute($itemnumber);
1025 my ($biblionumber) = $sth->fetchrow;
1027 # get request - need to find out if item is already
1028 # waiting in order to not send duplicate hold filled notifications
1031 # Find hold by id if we have it
1032 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1033 # Find item level hold for this item if there is one
1034 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1035 # Find record level hold if there is no item level hold
1036 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1038 return unless $hold;
1040 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1042 $hold->itemnumber($itemnumber);
1043 $hold->set_waiting($transferToDo);
1045 _koha_notify_reserve( $hold->reserve_id )
1046 if ( !$transferToDo && !$already_on_shelf );
1048 _FixPriority( { biblionumber => $biblionumber } );
1050 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1051 CartToShelf($itemnumber);
1057 =head2 ModReserveCancelAll
1059 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1061 function to cancel reserv,check other reserves, and transfer document if it's necessary
1065 sub ModReserveCancelAll {
1068 my ( $itemnumber, $borrowernumber ) = @_;
1070 #step 1 : cancel the reservation
1071 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1072 return unless $holds->count;
1073 $holds->next->cancel;
1075 #step 2 launch the subroutine of the others reserves
1076 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1078 return ( $messages, $nextreservinfo );
1081 =head2 ModReserveMinusPriority
1083 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1085 Reduce the values of queued list
1089 sub ModReserveMinusPriority {
1090 my ( $itemnumber, $reserve_id ) = @_;
1092 #first step update the value of the first person on reserv
1093 my $dbh = C4::Context->dbh;
1096 SET priority = 0 , itemnumber = ?
1097 WHERE reserve_id = ?
1099 my $sth_upd = $dbh->prepare($query);
1100 $sth_upd->execute( $itemnumber, $reserve_id );
1101 # second step update all others reserves
1102 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1105 =head2 IsAvailableForItemLevelRequest
1107 my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1109 Checks whether a given item record is available for an
1110 item-level hold request. An item is available if
1112 * it is not lost AND
1113 * it is not damaged AND
1114 * it is not withdrawn AND
1115 * a waiting or in transit reserve is placed on
1116 * does not have a not for loan value > 0
1118 Need to check the issuingrules onshelfholds column,
1119 if this is set items on the shelf can be placed on hold
1121 Note that IsAvailableForItemLevelRequest() does not
1122 check if the staff operator is authorized to place
1123 a request on the item - in particular,
1124 this routine does not check IndependentBranches
1125 and canreservefromotherbranches.
1129 sub IsAvailableForItemLevelRequest {
1131 my $borrower = shift;
1133 my $dbh = C4::Context->dbh;
1134 # must check the notforloan setting of the itemtype
1135 # FIXME - a lot of places in the code do this
1136 # or something similar - need to be
1138 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
1139 my $item_object = Koha::Items->find( $item->{itemnumber } );
1140 my $itemtype = $item_object->effective_itemtype;
1141 my $notforloan_per_itemtype
1142 = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1146 $notforloan_per_itemtype ||
1147 $item->{itemlost} ||
1148 $item->{notforloan} > 0 ||
1149 $item->{withdrawn} ||
1150 ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1152 my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item_object, patron => $patron } );
1154 if ( $on_shelf_holds == 1 ) {
1156 } elsif ( $on_shelf_holds == 2 ) {
1158 Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1160 my $any_available = 0;
1162 foreach my $i (@items) {
1164 my $circ_control_branch = C4::Circulation::_GetCircControlBranch( $i->unblessed(), $borrower );
1165 my $branchitemrule = C4::Circulation::GetBranchItemRule( $circ_control_branch, $i->itype );
1169 || $i->notforloan > 0
1172 || IsItemOnHoldAndFound( $i->id )
1174 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1175 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1176 || $branchitemrule->{holdallowed} == 1 && $borrower->{branchcode} ne $i->homebranch;
1179 return $any_available ? 0 : 1;
1180 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1181 return $item->{onloan} || IsItemOnHoldAndFound( $item->{itemnumber} );
1189 if (C4::Context->preference('item-level_itypes')) {
1190 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1191 # When GetItem is fixed, we can remove this
1192 $itype = $item->{itype};
1195 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1196 # So if we already have a biblioitems join when calling this function,
1197 # we don't need to access the database again
1198 $itype = $item->{itemtype};
1201 my $dbh = C4::Context->dbh;
1202 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1203 my $sth = $dbh->prepare($query);
1204 $sth->execute($item->{biblioitemnumber});
1205 if (my $data = $sth->fetchrow_hashref()){
1206 $itype = $data->{itemtype};
1212 =head2 AlterPriority
1214 AlterPriority( $where, $reserve_id );
1216 This function changes a reserve's priority up, down, to the top, or to the bottom.
1217 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1222 my ( $where, $reserve_id ) = @_;
1224 my $hold = Koha::Holds->find( $reserve_id );
1225 return unless $hold;
1227 if ( $hold->cancellationdate ) {
1228 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1232 if ( $where eq 'up' || $where eq 'down' ) {
1234 my $priority = $hold->priority;
1235 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1236 _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1238 } elsif ( $where eq 'top' ) {
1240 _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1242 } elsif ( $where eq 'bottom' ) {
1244 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1247 # FIXME Should return the new priority
1250 =head2 ToggleLowestPriority
1252 ToggleLowestPriority( $borrowernumber, $biblionumber );
1254 This function sets the lowestPriority field to true if is false, and false if it is true.
1258 sub ToggleLowestPriority {
1259 my ( $reserve_id ) = @_;
1261 my $dbh = C4::Context->dbh;
1263 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1264 $sth->execute( $reserve_id );
1266 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1269 =head2 ToggleSuspend
1271 ToggleSuspend( $reserve_id );
1273 This function sets the suspend field to true if is false, and false if it is true.
1274 If the reserve is currently suspended with a suspend_until date, that date will
1275 be cleared when it is unsuspended.
1280 my ( $reserve_id, $suspend_until ) = @_;
1282 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1284 my $hold = Koha::Holds->find( $reserve_id );
1286 if ( $hold->is_suspended ) {
1289 $hold->suspend_hold( $suspend_until );
1296 borrowernumber => $borrowernumber,
1297 [ biblionumber => $biblionumber, ]
1298 [ suspend_until => $suspend_until, ]
1299 [ suspend => $suspend ]
1302 This function accepts a set of hash keys as its parameters.
1303 It requires either borrowernumber or biblionumber, or both.
1305 suspend_until is wholly optional.
1312 my $borrowernumber = $params{'borrowernumber'} || undef;
1313 my $biblionumber = $params{'biblionumber'} || undef;
1314 my $suspend_until = $params{'suspend_until'} || undef;
1315 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1317 $suspend_until = eval { dt_from_string($suspend_until) }
1318 if ( defined($suspend_until) );
1320 return unless ( $borrowernumber || $biblionumber );
1323 $params->{found} = undef;
1324 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1325 $params->{biblionumber} = $biblionumber if $biblionumber;
1327 my @holds = Koha::Holds->search($params);
1330 map { $_->suspend_hold($suspend_until) } @holds;
1333 map { $_->resume() } @holds;
1341 reserve_id => $reserve_id,
1343 [ignoreSetLowestRank => $ignoreSetLowestRank]
1348 _FixPriority({ biblionumber => $biblionumber});
1350 This routine adjusts the priority of a hold request and holds
1353 In the first form, where a reserve_id is passed, the priority of the
1354 hold is set to supplied rank, and other holds for that bib are adjusted
1355 accordingly. If the rank is "del", the hold is cancelled. If no rank
1356 is supplied, all of the holds on that bib have their priority adjusted
1357 as if the second form had been used.
1359 In the second form, where a biblionumber is passed, the holds on that
1360 bib (that are not captured) are sorted in order of increasing priority,
1361 then have reserves.priority set so that the first non-captured hold
1362 has its priority set to 1, the second non-captured hold has its priority
1363 set to 2, and so forth.
1365 In both cases, holds that have the lowestPriority flag on are have their
1366 priority adjusted to ensure that they remain at the end of the line.
1368 Note that the ignoreSetLowestRank parameter is meant to be used only
1369 when _FixPriority calls itself.
1374 my ( $params ) = @_;
1375 my $reserve_id = $params->{reserve_id};
1376 my $rank = $params->{rank} // '';
1377 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1378 my $biblionumber = $params->{biblionumber};
1380 my $dbh = C4::Context->dbh;
1383 if ( $reserve_id ) {
1384 $hold = Koha::Holds->find( $reserve_id );
1385 return unless $hold;
1388 unless ( $biblionumber ) { # FIXME This is a very weird API
1389 $biblionumber = $hold->biblionumber;
1392 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1395 elsif ( $rank eq "W" || $rank eq "0" ) {
1397 # make sure priority for waiting or in-transit items is 0
1401 WHERE reserve_id = ?
1402 AND found IN ('W', 'T')
1404 my $sth = $dbh->prepare($query);
1405 $sth->execute( $reserve_id );
1411 SELECT reserve_id, borrowernumber, reservedate
1413 WHERE biblionumber = ?
1414 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1415 ORDER BY priority ASC
1417 my $sth = $dbh->prepare($query);
1418 $sth->execute( $biblionumber );
1419 while ( my $line = $sth->fetchrow_hashref ) {
1420 push( @priority, $line );
1423 # To find the matching index
1425 my $key = -1; # to allow for 0 to be a valid result
1426 for ( $i = 0 ; $i < @priority ; $i++ ) {
1427 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1428 $key = $i; # save the index
1433 # if index exists in array then move it to new position
1434 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1435 my $new_rank = $rank -
1436 1; # $new_rank is what you want the new index to be in the array
1437 my $moving_item = splice( @priority, $key, 1 );
1438 splice( @priority, $new_rank, 0, $moving_item );
1441 # now fix the priority on those that are left....
1445 WHERE reserve_id = ?
1447 $sth = $dbh->prepare($query);
1448 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1451 $priority[$j]->{'reserve_id'}
1455 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1458 unless ( $ignoreSetLowestRank ) {
1459 while ( my $res = $sth->fetchrow_hashref() ) {
1461 reserve_id => $res->{'reserve_id'},
1463 ignoreSetLowestRank => 1
1469 =head2 _Findgroupreserve
1471 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1473 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1474 first match found. If neither, then we look for non-holds-queue based holds.
1475 Lookahead is the number of days to look in advance.
1477 C<&_Findgroupreserve> returns :
1478 C<@results> is an array of references-to-hash whose keys are mostly
1479 fields from the reserves table of the Koha database, plus
1480 C<biblioitemnumber>.
1484 sub _Findgroupreserve {
1485 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1486 my $dbh = C4::Context->dbh;
1488 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1489 # check for exact targeted match
1490 my $item_level_target_query = qq{
1491 SELECT reserves.biblionumber AS biblionumber,
1492 reserves.borrowernumber AS borrowernumber,
1493 reserves.reservedate AS reservedate,
1494 reserves.branchcode AS branchcode,
1495 reserves.cancellationdate AS cancellationdate,
1496 reserves.found AS found,
1497 reserves.reservenotes AS reservenotes,
1498 reserves.priority AS priority,
1499 reserves.timestamp AS timestamp,
1500 biblioitems.biblioitemnumber AS biblioitemnumber,
1501 reserves.itemnumber AS itemnumber,
1502 reserves.reserve_id AS reserve_id,
1503 reserves.itemtype AS itemtype
1505 JOIN biblioitems USING (biblionumber)
1506 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1509 AND item_level_request = 1
1511 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1515 my $sth = $dbh->prepare($item_level_target_query);
1516 $sth->execute($itemnumber, $lookahead||0);
1518 if ( my $data = $sth->fetchrow_hashref ) {
1519 push( @results, $data )
1520 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1522 return @results if @results;
1524 # check for title-level targeted match
1525 my $title_level_target_query = qq{
1526 SELECT reserves.biblionumber AS biblionumber,
1527 reserves.borrowernumber AS borrowernumber,
1528 reserves.reservedate AS reservedate,
1529 reserves.branchcode AS branchcode,
1530 reserves.cancellationdate AS cancellationdate,
1531 reserves.found AS found,
1532 reserves.reservenotes AS reservenotes,
1533 reserves.priority AS priority,
1534 reserves.timestamp AS timestamp,
1535 biblioitems.biblioitemnumber AS biblioitemnumber,
1536 reserves.itemnumber AS itemnumber,
1537 reserves.reserve_id AS reserve_id,
1538 reserves.itemtype AS itemtype
1540 JOIN biblioitems USING (biblionumber)
1541 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1544 AND item_level_request = 0
1545 AND hold_fill_targets.itemnumber = ?
1546 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1550 $sth = $dbh->prepare($title_level_target_query);
1551 $sth->execute($itemnumber, $lookahead||0);
1553 if ( my $data = $sth->fetchrow_hashref ) {
1554 push( @results, $data )
1555 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1557 return @results if @results;
1560 SELECT reserves.biblionumber AS biblionumber,
1561 reserves.borrowernumber AS borrowernumber,
1562 reserves.reservedate AS reservedate,
1563 reserves.waitingdate AS waitingdate,
1564 reserves.branchcode AS branchcode,
1565 reserves.cancellationdate AS cancellationdate,
1566 reserves.found AS found,
1567 reserves.reservenotes AS reservenotes,
1568 reserves.priority AS priority,
1569 reserves.timestamp AS timestamp,
1570 reserves.itemnumber AS itemnumber,
1571 reserves.reserve_id AS reserve_id,
1572 reserves.itemtype AS itemtype
1574 WHERE reserves.biblionumber = ?
1575 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1576 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1580 $sth = $dbh->prepare($query);
1581 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1583 while ( my $data = $sth->fetchrow_hashref ) {
1584 push( @results, $data )
1585 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1590 =head2 _koha_notify_reserve
1592 _koha_notify_reserve( $hold->reserve_id );
1594 Sends a notification to the patron that their hold has been filled (through
1595 ModReserveAffect, _not_ ModReserveFill)
1597 The letter code for this notice may be found using the following query:
1599 select distinct letter_code
1600 from message_transports
1601 inner join message_attributes using (message_attribute_id)
1602 where message_name = 'Hold_Filled'
1604 This will probably sipmly be 'HOLD', but because it is defined in the database,
1605 it is subject to addition or change.
1607 The following tables are availalbe witin the notice:
1618 sub _koha_notify_reserve {
1619 my $reserve_id = shift;
1620 my $hold = Koha::Holds->find($reserve_id);
1621 my $borrowernumber = $hold->borrowernumber;
1623 my $patron = Koha::Patrons->find( $borrowernumber );
1625 # Try to get the borrower's email address
1626 my $to_address = $patron->notice_email_address;
1628 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1629 borrowernumber => $borrowernumber,
1630 message_name => 'Hold_Filled'
1633 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1635 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1637 my %letter_params = (
1638 module => 'reserves',
1639 branchcode => $hold->branchcode,
1640 lang => $patron->lang,
1642 'branches' => $library,
1643 'borrowers' => $patron->unblessed,
1644 'biblio' => $hold->biblionumber,
1645 'biblioitems' => $hold->biblionumber,
1646 'reserves' => $hold->unblessed,
1647 'items' => $hold->itemnumber,
1651 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.
1652 my $send_notification = sub {
1653 my ( $mtt, $letter_code ) = (@_);
1654 return unless defined $letter_code;
1655 $letter_params{letter_code} = $letter_code;
1656 $letter_params{message_transport_type} = $mtt;
1657 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1659 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1663 C4::Letters::EnqueueLetter( {
1665 borrowernumber => $borrowernumber,
1666 from_address => $admin_email_address,
1667 message_transport_type => $mtt,
1671 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1673 ( $mtt eq 'email' and not $to_address ) # No email address
1674 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1675 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1678 &$send_notification($mtt, $letter_code);
1679 $notification_sent++;
1681 #Making sure that a print notification is sent if no other transport types can be utilized.
1682 if (! $notification_sent) {
1683 &$send_notification('print', 'HOLD');
1688 =head2 _ShiftPriorityByDateAndPriority
1690 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1692 This increments the priority of all reserves after the one
1693 with either the lowest date after C<$reservedate>
1694 or the lowest priority after C<$priority>.
1696 It effectively makes room for a new reserve to be inserted with a certain
1697 priority, which is returned.
1699 This is most useful when the reservedate can be set by the user. It allows
1700 the new reserve to be placed before other reserves that have a later
1701 reservedate. Since priority also is set by the form in reserves/request.pl
1702 the sub accounts for that too.
1706 sub _ShiftPriorityByDateAndPriority {
1707 my ( $biblio, $resdate, $new_priority ) = @_;
1709 my $dbh = C4::Context->dbh;
1710 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1711 my $sth = $dbh->prepare( $query );
1712 $sth->execute( $biblio, $resdate, $new_priority );
1713 my $min_priority = $sth->fetchrow;
1714 # if no such matches are found, $new_priority remains as original value
1715 $new_priority = $min_priority if ( $min_priority );
1717 # Shift the priority up by one; works in conjunction with the next SQL statement
1718 $query = "UPDATE reserves
1719 SET priority = priority+1
1720 WHERE biblionumber = ?
1721 AND borrowernumber = ?
1724 my $sth_update = $dbh->prepare( $query );
1726 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1727 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1728 $sth = $dbh->prepare( $query );
1729 $sth->execute( $new_priority, $biblio );
1730 while ( my $row = $sth->fetchrow_hashref ) {
1731 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1734 return $new_priority; # so the caller knows what priority they wind up receiving
1739 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1741 Use when checking out an item to handle reserves
1742 If $cancelreserve boolean is set to true, it will remove existing reserve
1747 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1749 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1750 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
1753 my $biblionumber = $res->{biblionumber};
1755 if ($res->{borrowernumber} == $borrowernumber) {
1756 ModReserveFill($res);
1760 # The item is reserved by someone else.
1761 # Find this item in the reserves
1764 foreach (@$all_reserves) {
1765 $_->{'borrowernumber'} == $borrowernumber or next;
1766 $_->{'biblionumber'} == $biblionumber or next;
1773 # The item is reserved by the current patron
1774 ModReserveFill($borr_res);
1777 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1778 RevertWaitingStatus({ itemnumber => $itemnumber });
1780 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1781 my $hold = Koha::Holds->find( $res->{reserve_id} );
1789 MergeHolds($dbh,$to_biblio, $from_biblio);
1791 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1796 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1797 my $sth = $dbh->prepare(
1798 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1800 $sth->execute($from_biblio);
1801 if ( my $data = $sth->fetchrow_hashref() ) {
1803 # holds exist on old record, if not we don't need to do anything
1804 $sth = $dbh->prepare(
1805 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1806 $sth->execute( $to_biblio, $from_biblio );
1809 # don't reorder those already waiting
1811 $sth = $dbh->prepare(
1812 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1814 my $upd_sth = $dbh->prepare(
1815 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1816 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1818 $sth->execute( $to_biblio, 'W', 'T' );
1820 while ( my $reserve = $sth->fetchrow_hashref() ) {
1822 $priority, $to_biblio,
1823 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1824 $reserve->{'itemnumber'}
1831 =head2 RevertWaitingStatus
1833 RevertWaitingStatus({ itemnumber => $itemnumber });
1835 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1837 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1838 item level hold, even if it was only a bibliolevel hold to
1839 begin with. This is because we can no longer know if a hold
1840 was item-level or bib-level after a hold has been set to
1845 sub RevertWaitingStatus {
1846 my ( $params ) = @_;
1847 my $itemnumber = $params->{'itemnumber'};
1849 return unless ( $itemnumber );
1851 my $dbh = C4::Context->dbh;
1853 ## Get the waiting reserve we want to revert
1855 SELECT * FROM reserves
1856 WHERE itemnumber = ?
1857 AND found IS NOT NULL
1859 my $sth = $dbh->prepare( $query );
1860 $sth->execute( $itemnumber );
1861 my $reserve = $sth->fetchrow_hashref();
1863 ## Increment the priority of all other non-waiting
1864 ## reserves for this bib record
1868 priority = priority + 1
1874 $sth = $dbh->prepare( $query );
1875 $sth->execute( $reserve->{'biblionumber'} );
1877 ## Fix up the currently waiting reserve
1887 $sth = $dbh->prepare( $query );
1888 $sth->execute( $reserve->{'reserve_id'} );
1889 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1896 branchcode => $branchcode,
1897 borrowernumber => $borrowernumber,
1898 biblionumber => $biblionumber,
1899 [ itemnumber => $itemnumber, ]
1900 [ barcode => $barcode, ]
1904 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1906 The letter code will be HOLD_SLIP, and the following tables are
1907 available within the slip:
1920 my $branchcode = $args->{branchcode};
1921 my $borrowernumber = $args->{borrowernumber};
1922 my $biblionumber = $args->{biblionumber};
1923 my $itemnumber = $args->{itemnumber};
1924 my $barcode = $args->{barcode};
1927 my $patron = Koha::Patrons->find($borrowernumber);
1930 if ($itemnumber || $barcode ) {
1931 $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
1933 $hold = Koha::Holds->search(
1935 biblionumber => $biblionumber,
1936 borrowernumber => $borrowernumber,
1937 itemnumber => $itemnumber
1942 $hold = Koha::Holds->search(
1944 biblionumber => $biblionumber,
1945 borrowernumber => $borrowernumber
1950 return unless $hold;
1951 my $reserve = $hold->unblessed;
1953 return C4::Letters::GetPreparedLetter (
1954 module => 'circulation',
1955 letter_code => 'HOLD_SLIP',
1956 branchcode => $branchcode,
1957 lang => $patron->lang,
1959 'reserves' => $reserve,
1960 'branches' => $reserve->{branchcode},
1961 'borrowers' => $reserve->{borrowernumber},
1962 'biblio' => $reserve->{biblionumber},
1963 'biblioitems' => $reserve->{biblionumber},
1964 'items' => $reserve->{itemnumber},
1969 =head2 GetReservesControlBranch
1971 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
1973 Return the branchcode to be used to determine which reserves
1974 policy applies to a transaction.
1976 C<$item> is a hashref for an item. Only 'homebranch' is used.
1978 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
1982 sub GetReservesControlBranch {
1983 my ( $item, $borrower ) = @_;
1985 my $reserves_control = C4::Context->preference('ReservesControlBranch');
1988 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
1989 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
1995 =head2 CalculatePriority
1997 my $p = CalculatePriority($biblionumber, $resdate);
1999 Calculate priority for a new reserve on biblionumber, placing it at
2000 the end of the line of all holds whose start date falls before
2001 the current system time and that are neither on the hold shelf
2004 The reserve date parameter is optional; if it is supplied, the
2005 priority is based on the set of holds whose start date falls before
2006 the parameter value.
2008 After calculation of this priority, it is recommended to call
2009 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2014 sub CalculatePriority {
2015 my ( $biblionumber, $resdate ) = @_;
2018 SELECT COUNT(*) FROM reserves
2019 WHERE biblionumber = ?
2021 AND (found IS NULL OR found = '')
2023 #skip found==W or found==T (waiting or transit holds)
2025 $sql.= ' AND ( reservedate <= ? )';
2028 $sql.= ' AND ( reservedate < NOW() )';
2030 my $dbh = C4::Context->dbh();
2031 my @row = $dbh->selectrow_array(
2034 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2037 return @row ? $row[0]+1 : 1;
2040 =head2 IsItemOnHoldAndFound
2042 my $bool = IsItemFoundHold( $itemnumber );
2044 Returns true if the item is currently on hold
2045 and that hold has a non-null found status ( W, T, etc. )
2049 sub IsItemOnHoldAndFound {
2050 my ($itemnumber) = @_;
2052 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2054 my $found = $rs->count(
2056 itemnumber => $itemnumber,
2057 found => { '!=' => undef }
2064 =head2 GetMaxPatronHoldsForRecord
2066 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2068 For multiple holds on a given record for a given patron, the max
2069 number of record level holds that a patron can be placed is the highest
2070 value of the holds_per_record rule for each item if the record for that
2071 patron. This subroutine finds and returns the highest holds_per_record
2072 rule value for a given patron id and record id.
2076 sub GetMaxPatronHoldsForRecord {
2077 my ( $borrowernumber, $biblionumber ) = @_;
2079 my $patron = Koha::Patrons->find($borrowernumber);
2080 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2082 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2084 my $categorycode = $patron->categorycode;
2086 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2089 foreach my $item (@items) {
2090 my $itemtype = $item->effective_itemtype();
2092 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2094 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2095 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2096 $max = $holds_per_record if $holds_per_record > $max;
2104 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2106 Returns the matching hold related issuingrule fields for a given
2107 patron category, itemtype, and library.
2112 my ( $categorycode, $itemtype, $branchcode ) = @_;
2114 my $dbh = C4::Context->dbh;
2116 my $sth = $dbh->prepare(
2118 SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record
2120 WHERE (categorycode in (?,'*') )
2121 AND (itemtype IN (?,'*'))
2122 AND (branchcode IN (?,'*'))
2123 ORDER BY categorycode DESC,
2129 $sth->execute( $categorycode, $itemtype, $branchcode );
2131 return $sth->fetchrow_hashref();
2136 Koha Development Team <http://koha-community.org/>