Bug 19299: Replace C4::Reserves::GetReservesForBranch with Koha::Holds->waiting
[koha.git] / C4 / Reserves.pm
blob06cde18228e16185a41090731db1ad59137d845e
1 package C4::Reserves;
3 # Copyright 2000-2002 Katipo Communications
4 # 2006 SAN Ouest Provence
5 # 2007-2010 BibLibre Paul POULAIN
6 # 2011 Catalyst IT
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>.
24 use strict;
25 #use warnings; FIXME - Bug 2505
26 use C4::Context;
27 use C4::Biblio;
28 use C4::Members;
29 use C4::Items;
30 use C4::Circulation;
31 use C4::Accounts;
33 # for _koha_notify_reserve
34 use C4::Members::Messaging;
35 use C4::Members qw();
36 use C4::Letters;
37 use C4::Log;
39 use Koha::Biblios;
40 use Koha::DateUtils;
41 use Koha::Calendar;
42 use Koha::Database;
43 use Koha::Hold;
44 use Koha::Old::Hold;
45 use Koha::Holds;
46 use Koha::Libraries;
47 use Koha::IssuingRules;
48 use Koha::Items;
49 use Koha::ItemTypes;
50 use Koha::Patrons;
52 use List::MoreUtils qw( firstidx any );
53 use Carp;
54 use Data::Dumper;
56 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
58 =head1 NAME
60 C4::Reserves - Koha functions for dealing with reservation.
62 =head1 SYNOPSIS
64 use C4::Reserves;
66 =head1 DESCRIPTION
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
97 =head1 FUNCTIONS
99 =cut
101 BEGIN {
102 require Exporter;
103 @ISA = qw(Exporter);
104 @EXPORT = qw(
105 &AddReserve
107 &GetReserveStatus
109 &GetOtherReserves
111 &ModReserveFill
112 &ModReserveAffect
113 &ModReserve
114 &ModReserveStatus
115 &ModReserveCancelAll
116 &ModReserveMinusPriority
117 &MoveReserve
119 &CheckReserves
120 &CanBookBeReserved
121 &CanItemBeReserved
122 &CanReserveBeCanceledFromOpac
123 &CancelExpiredReserves
125 &AutoUnsuspendReserves
127 &IsAvailableForItemLevelRequest
129 &OPACItemHoldsAllowed
131 &AlterPriority
132 &ToggleLowestPriority
134 &ReserveSlip
135 &ToggleSuspend
136 &SuspendAll
138 &GetReservesControlBranch
140 IsItemOnHoldAndFound
142 GetMaxPatronHoldsForRecord
144 @EXPORT_OK = qw( MergeHolds );
147 =head2 AddReserve
149 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
151 Adds reserve and generates HOLDPLACED message.
153 The following tables are available witin the HOLDPLACED message:
155 branches
156 borrowers
157 biblio
158 biblioitems
159 items
160 reserves
162 =cut
164 sub AddReserve {
165 my (
166 $branch, $borrowernumber, $biblionumber, $bibitems,
167 $priority, $resdate, $expdate, $notes,
168 $title, $checkitem, $found, $itemtype
169 ) = @_;
171 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
172 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
174 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
176 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
178 # Make room in reserves for this before those of a later reserve date
179 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
182 my $waitingdate;
184 # If the reserv had the waiting status, we had the value of the resdate
185 if ( $found eq 'W' ) {
186 $waitingdate = $resdate;
189 # Don't add itemtype limit if specific item is selected
190 $itemtype = undef if $checkitem;
192 # updates take place here
193 my $hold = Koha::Hold->new(
195 borrowernumber => $borrowernumber,
196 biblionumber => $biblionumber,
197 reservedate => $resdate,
198 branchcode => $branch,
199 priority => $priority,
200 reservenotes => $notes,
201 itemnumber => $checkitem,
202 found => $found,
203 waitingdate => $waitingdate,
204 expirationdate => $expdate,
205 itemtype => $itemtype,
207 )->store();
209 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
210 if C4::Context->preference('HoldsLog');
212 my $reserve_id = $hold->id();
214 # add a reserve fee if needed
215 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
216 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
217 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
220 _FixPriority({ biblionumber => $biblionumber});
222 # Send e-mail to librarian if syspref is active
223 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
224 my $patron = Koha::Patrons->find( $borrowernumber );
225 my $library = $patron->library;
226 if ( my $letter = C4::Letters::GetPreparedLetter (
227 module => 'reserves',
228 letter_code => 'HOLDPLACED',
229 branchcode => $branch,
230 lang => $patron->lang,
231 tables => {
232 'branches' => $library->unblessed,
233 'borrowers' => $patron->unblessed,
234 'biblio' => $biblionumber,
235 'biblioitems' => $biblionumber,
236 'items' => $checkitem,
237 'reserves' => $hold->unblessed,
239 ) ) {
241 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
243 C4::Letters::EnqueueLetter(
244 { letter => $letter,
245 borrowernumber => $borrowernumber,
246 message_transport_type => 'email',
247 from_address => $admin_email_address,
248 to_address => $admin_email_address,
254 return $reserve_id;
257 =head2 CanBookBeReserved
259 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber)
260 if ($canReserve eq 'OK') { #We can reserve this Item! }
262 See CanItemBeReserved() for possible return values.
264 =cut
266 sub CanBookBeReserved{
267 my ($borrowernumber, $biblionumber) = @_;
269 my $items = GetItemnumbersForBiblio($biblionumber);
270 #get items linked via host records
271 my @hostitems = get_hostitemnumbers_of($biblionumber);
272 if (@hostitems){
273 push (@$items,@hostitems);
276 my $canReserve;
277 foreach my $item (@$items) {
278 $canReserve = CanItemBeReserved( $borrowernumber, $item );
279 return 'OK' if $canReserve eq 'OK';
281 return $canReserve;
284 =head2 CanItemBeReserved
286 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber)
287 if ($canReserve eq 'OK') { #We can reserve this Item! }
289 @RETURNS OK, if the Item can be reserved.
290 ageRestricted, if the Item is age restricted for this borrower.
291 damaged, if the Item is damaged.
292 cannotReserveFromOtherBranches, if syspref 'canreservefromotherbranches' is OK.
293 tooManyReserves, if the borrower has exceeded his maximum reserve amount.
294 notReservable, if holds on this item are not allowed
296 =cut
298 sub CanItemBeReserved {
299 my ( $borrowernumber, $itemnumber ) = @_;
301 my $dbh = C4::Context->dbh;
302 my $ruleitemtype; # itemtype of the matching issuing rule
303 my $allowedreserves = 0; # Total number of holds allowed across all records
304 my $holds_per_record = 1; # Total number of holds allowed for this one given record
306 # we retrieve borrowers and items informations #
307 # item->{itype} will come for biblioitems if necessery
308 my $item = GetItem($itemnumber);
309 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
310 my $patron = Koha::Patrons->find( $borrowernumber );
311 my $borrower = $patron->unblessed;
313 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
314 return 'damaged'
315 if ( $item->{damaged}
316 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
318 # Check for the age restriction
319 my ( $ageRestriction, $daysToAgeRestriction ) =
320 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
321 return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0;
323 # Check that the patron doesn't have an item level hold on this item already
324 return 'itemAlreadyOnHold'
325 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
327 my $controlbranch = C4::Context->preference('ReservesControlBranch');
329 my $querycount = q{
330 SELECT count(*) AS count
331 FROM reserves
332 LEFT JOIN items USING (itemnumber)
333 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
334 LEFT JOIN borrowers USING (borrowernumber)
335 WHERE borrowernumber = ?
338 my $branchcode = "";
339 my $branchfield = "reserves.branchcode";
341 if ( $controlbranch eq "ItemHomeLibrary" ) {
342 $branchfield = "items.homebranch";
343 $branchcode = $item->{homebranch};
345 elsif ( $controlbranch eq "PatronLibrary" ) {
346 $branchfield = "borrowers.branchcode";
347 $branchcode = $borrower->{branchcode};
350 # we retrieve rights
351 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) {
352 $ruleitemtype = $rights->{itemtype};
353 $allowedreserves = $rights->{reservesallowed};
354 $holds_per_record = $rights->{holds_per_record};
356 else {
357 $ruleitemtype = '*';
360 $item = Koha::Items->find( $itemnumber );
361 my $holds = Koha::Holds->search(
363 borrowernumber => $borrowernumber,
364 biblionumber => $item->biblionumber,
365 found => undef, # Found holds don't count against a patron's holds limit
368 if ( $holds->count() >= $holds_per_record ) {
369 return "tooManyHoldsForThisRecord";
372 # we retrieve count
374 $querycount .= "AND $branchfield = ?";
376 # If using item-level itypes, fall back to the record
377 # level itemtype if the hold has no associated item
378 $querycount .=
379 C4::Context->preference('item-level_itypes')
380 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
381 : " AND biblioitems.itemtype = ?"
382 if ( $ruleitemtype ne "*" );
384 my $sthcount = $dbh->prepare($querycount);
386 if ( $ruleitemtype eq "*" ) {
387 $sthcount->execute( $borrowernumber, $branchcode );
389 else {
390 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
393 my $reservecount = "0";
394 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
395 $reservecount = $rowcount->{count};
398 # we check if it's ok or not
399 if ( $reservecount >= $allowedreserves ) {
400 return 'tooManyReserves';
403 my $circ_control_branch =
404 C4::Circulation::_GetCircControlBranch( $item->unblessed(), $borrower );
405 my $branchitemrule =
406 C4::Circulation::GetBranchItemRule( $circ_control_branch, $item->itype );
408 if ( $branchitemrule->{holdallowed} == 0 ) {
409 return 'notReservable';
412 if ( $branchitemrule->{holdallowed} == 1
413 && $borrower->{branchcode} ne $item->homebranch )
415 return 'cannotReserveFromOtherBranches';
418 # If reservecount is ok, we check item branch if IndependentBranches is ON
419 # and canreservefromotherbranches is OFF
420 if ( C4::Context->preference('IndependentBranches')
421 and !C4::Context->preference('canreservefromotherbranches') )
423 my $itembranch = $item->homebranch;
424 if ( $itembranch ne $borrower->{branchcode} ) {
425 return 'cannotReserveFromOtherBranches';
429 return 'OK';
432 =head2 CanReserveBeCanceledFromOpac
434 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
436 returns 1 if reserve can be cancelled by user from OPAC.
437 First check if reserve belongs to user, next checks if reserve is not in
438 transfer or waiting status
440 =cut
442 sub CanReserveBeCanceledFromOpac {
443 my ($reserve_id, $borrowernumber) = @_;
445 return unless $reserve_id and $borrowernumber;
446 my $reserve = Koha::Holds->find($reserve_id);
448 return 0 unless $reserve->borrowernumber == $borrowernumber;
449 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
451 return 1;
455 =head2 GetOtherReserves
457 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
459 Check queued list of this document and check if this document must be transferred
461 =cut
463 sub GetOtherReserves {
464 my ($itemnumber) = @_;
465 my $messages;
466 my $nextreservinfo;
467 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
468 if ($checkreserves) {
469 my $iteminfo = GetItem($itemnumber);
470 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
471 $messages->{'transfert'} = $checkreserves->{'branchcode'};
472 #minus priorities of others reservs
473 ModReserveMinusPriority(
474 $itemnumber,
475 $checkreserves->{'reserve_id'},
478 #launch the subroutine dotransfer
479 C4::Items::ModItemTransfer(
480 $itemnumber,
481 $iteminfo->{'holdingbranch'},
482 $checkreserves->{'branchcode'}
487 #step 2b : case of a reservation on the same branch, set the waiting status
488 else {
489 $messages->{'waiting'} = 1;
490 ModReserveMinusPriority(
491 $itemnumber,
492 $checkreserves->{'reserve_id'},
494 ModReserveStatus($itemnumber,'W');
497 $nextreservinfo = $checkreserves->{'borrowernumber'};
500 return ( $messages, $nextreservinfo );
503 =head2 ChargeReserveFee
505 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
507 Charge the fee for a reserve (if $fee > 0)
509 =cut
511 sub ChargeReserveFee {
512 my ( $borrowernumber, $fee, $title ) = @_;
513 return if !$fee || $fee==0; # the last test is needed to include 0.00
514 my $accquery = qq{
515 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
517 my $dbh = C4::Context->dbh;
518 my $nextacctno = &getnextacctno( $borrowernumber );
519 $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
522 =head2 GetReserveFee
524 $fee = GetReserveFee( $borrowernumber, $biblionumber );
526 Calculate the fee for a reserve (if applicable).
528 =cut
530 sub GetReserveFee {
531 my ( $borrowernumber, $biblionumber ) = @_;
532 my $borquery = qq{
533 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
535 my $issue_qry = qq{
536 SELECT COUNT(*) FROM items
537 LEFT JOIN issues USING (itemnumber)
538 WHERE items.biblionumber=? AND issues.issue_id IS NULL
540 my $holds_qry = qq{
541 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
544 my $dbh = C4::Context->dbh;
545 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
546 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
547 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
548 # This is a reconstruction of the old code:
549 # Compare number of items with items issued, and optionally check holds
550 # If not all items are issued and there are no holds: charge no fee
551 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
552 my ( $notissued, $reserved );
553 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
554 ( $biblionumber ) );
555 if( $notissued ) {
556 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
557 ( $biblionumber, $borrowernumber ) );
558 $fee = 0 if $reserved == 0;
561 return $fee;
564 =head2 GetReserveStatus
566 $reservestatus = GetReserveStatus($itemnumber);
568 Takes an itemnumber and returns the status of the reserve placed on it.
569 If several reserves exist, the reserve with the lower priority is given.
571 =cut
573 ## FIXME: I don't think this does what it thinks it does.
574 ## It only ever checks the first reserve result, even though
575 ## multiple reserves for that bib can have the itemnumber set
576 ## the sub is only used once in the codebase.
577 sub GetReserveStatus {
578 my ($itemnumber) = @_;
580 my $dbh = C4::Context->dbh;
582 my ($sth, $found, $priority);
583 if ( $itemnumber ) {
584 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
585 $sth->execute($itemnumber);
586 ($found, $priority) = $sth->fetchrow_array;
589 if(defined $found) {
590 return 'Waiting' if $found eq 'W' and $priority == 0;
591 return 'Finished' if $found eq 'F';
594 return 'Reserved' if $priority > 0;
596 return ''; # empty string here will remove need for checking undef, or less log lines
599 =head2 CheckReserves
601 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
602 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
603 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
605 Find a book in the reserves.
607 C<$itemnumber> is the book's item number.
608 C<$lookahead> is the number of days to look in advance for future reserves.
610 As I understand it, C<&CheckReserves> looks for the given item in the
611 reserves. If it is found, that's a match, and C<$status> is set to
612 C<Waiting>.
614 Otherwise, it finds the most important item in the reserves with the
615 same biblio number as this book (I'm not clear on this) and returns it
616 with C<$status> set to C<Reserved>.
618 C<&CheckReserves> returns a two-element list:
620 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
622 C<$reserve> is the reserve item that matched. It is a
623 reference-to-hash whose keys are mostly the fields of the reserves
624 table in the Koha database.
626 =cut
628 sub CheckReserves {
629 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
630 my $dbh = C4::Context->dbh;
631 my $sth;
632 my $select;
633 if (C4::Context->preference('item-level_itypes')){
634 $select = "
635 SELECT items.biblionumber,
636 items.biblioitemnumber,
637 itemtypes.notforloan,
638 items.notforloan AS itemnotforloan,
639 items.itemnumber,
640 items.damaged,
641 items.homebranch,
642 items.holdingbranch
643 FROM items
644 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
645 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
648 else {
649 $select = "
650 SELECT items.biblionumber,
651 items.biblioitemnumber,
652 itemtypes.notforloan,
653 items.notforloan AS itemnotforloan,
654 items.itemnumber,
655 items.damaged,
656 items.homebranch,
657 items.holdingbranch
658 FROM items
659 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
660 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
664 if ($item) {
665 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
666 $sth->execute($item);
668 else {
669 $sth = $dbh->prepare("$select WHERE barcode = ?");
670 $sth->execute($barcode);
672 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
673 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
675 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
677 return unless $itemnumber; # bail if we got nothing.
679 # if item is not for loan it cannot be reserved either.....
680 # except where items.notforloan < 0 : This indicates the item is holdable.
681 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
683 # Find this item in the reserves
684 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
686 # $priority and $highest are used to find the most important item
687 # in the list returned by &_Findgroupreserve. (The lower $priority,
688 # the more important the item.)
689 # $highest is the most important item we've seen so far.
690 my $highest;
691 if (scalar @reserves) {
692 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
693 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
694 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
696 my $priority = 10000000;
697 foreach my $res (@reserves) {
698 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
699 if ($res->{'found'} eq 'W') {
700 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
701 } else {
702 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
704 } else {
705 my $patron;
706 my $iteminfo;
707 my $local_hold_match;
709 if ($LocalHoldsPriority) {
710 $patron = Koha::Patrons->find( $res->{borrowernumber} );
711 $iteminfo = C4::Items::GetItem($itemnumber);
713 my $local_holds_priority_item_branchcode =
714 $iteminfo->{$LocalHoldsPriorityItemControl};
715 my $local_holds_priority_patron_branchcode =
716 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
717 ? $res->{branchcode}
718 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
719 ? $patron->branchcode
720 : undef;
721 $local_hold_match =
722 $local_holds_priority_item_branchcode eq
723 $local_holds_priority_patron_branchcode;
726 # See if this item is more important than what we've got so far
727 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
728 $iteminfo ||= C4::Items::GetItem($itemnumber);
729 next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
730 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
731 my $branch = GetReservesControlBranch( $iteminfo, $patron->unblessed );
732 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
733 next if ($branchitemrule->{'holdallowed'} == 0);
734 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
735 next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
736 $priority = $res->{'priority'};
737 $highest = $res;
738 last if $local_hold_match;
744 # If we get this far, then no exact match was found.
745 # We return the most important (i.e. next) reservation.
746 if ($highest) {
747 $highest->{'itemnumber'} = $item;
748 return ( "Reserved", $highest, \@reserves );
751 return ( '' );
754 =head2 CancelExpiredReserves
756 CancelExpiredReserves();
758 Cancels all reserves with an expiration date from before today.
760 =cut
762 sub CancelExpiredReserves {
763 my $today = dt_from_string();
764 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
765 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
767 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
768 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
769 $params->{found} = undef unless $expireWaiting;
771 # FIXME To move to Koha::Holds->search_expired (?)
772 my $holds = Koha::Holds->search( $params );
774 while ( my $hold = $holds->next ) {
775 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
777 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
779 my $cancel_params = {};
780 if ( $hold->found eq 'W' ) {
781 $cancel_params->{charge_cancel_fee} = 1;
783 $hold->cancel( $cancel_params );
787 =head2 AutoUnsuspendReserves
789 AutoUnsuspendReserves();
791 Unsuspends all suspended reserves with a suspend_until date from before today.
793 =cut
795 sub AutoUnsuspendReserves {
796 my $today = dt_from_string();
798 my @holds = Koha::Holds->search( { suspend_until => { '<' => $today->ymd() } } );
800 map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
803 =head2 ModReserve
805 ModReserve({ rank => $rank,
806 reserve_id => $reserve_id,
807 branchcode => $branchcode
808 [, itemnumber => $itemnumber ]
809 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
812 Change a hold request's priority or cancel it.
814 C<$rank> specifies the effect of the change. If C<$rank>
815 is 'W' or 'n', nothing happens. This corresponds to leaving a
816 request alone when changing its priority in the holds queue
817 for a bib.
819 If C<$rank> is 'del', the hold request is cancelled.
821 If C<$rank> is an integer greater than zero, the priority of
822 the request is set to that value. Since priority != 0 means
823 that the item is not waiting on the hold shelf, setting the
824 priority to a non-zero value also sets the request's found
825 status and waiting date to NULL.
827 The optional C<$itemnumber> parameter is used only when
828 C<$rank> is a non-zero integer; if supplied, the itemnumber
829 of the hold request is set accordingly; if omitted, the itemnumber
830 is cleared.
832 B<FIXME:> Note that the forgoing can have the effect of causing
833 item-level hold requests to turn into title-level requests. This
834 will be fixed once reserves has separate columns for requested
835 itemnumber and supplying itemnumber.
837 =cut
839 sub ModReserve {
840 my ( $params ) = @_;
842 my $rank = $params->{'rank'};
843 my $reserve_id = $params->{'reserve_id'};
844 my $branchcode = $params->{'branchcode'};
845 my $itemnumber = $params->{'itemnumber'};
846 my $suspend_until = $params->{'suspend_until'};
847 my $borrowernumber = $params->{'borrowernumber'};
848 my $biblionumber = $params->{'biblionumber'};
850 return if $rank eq "W";
851 return if $rank eq "n";
853 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
855 my $hold;
856 unless ( $reserve_id ) {
857 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
858 return unless $holds->count; # FIXME Should raise an exception
859 $hold = $holds->next;
860 $reserve_id = $hold->reserve_id;
863 $hold ||= Koha::Holds->find($reserve_id);
865 if ( $rank eq "del" ) {
866 $hold->cancel;
868 elsif ($rank =~ /^\d+/ and $rank > 0) {
869 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
870 if C4::Context->preference('HoldsLog');
872 $hold->set(
874 priority => $rank,
875 branchcode => $branchcode,
876 itemnumber => $itemnumber,
877 found => undef,
878 waitingdate => undef
880 )->store();
882 if ( defined( $suspend_until ) ) {
883 if ( $suspend_until ) {
884 $suspend_until = eval { dt_from_string( $suspend_until ) };
885 $hold->suspend_hold( $suspend_until );
886 } else {
887 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
888 # If the hold is not suspended, this does nothing.
889 $hold->set( { suspend_until => undef } )->store();
893 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
897 =head2 ModReserveFill
899 &ModReserveFill($reserve);
901 Fill a reserve. If I understand this correctly, this means that the
902 reserved book has been found and given to the patron who reserved it.
904 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
905 whose keys are fields from the reserves table in the Koha database.
907 =cut
909 sub ModReserveFill {
910 my ($res) = @_;
911 my $reserve_id = $res->{'reserve_id'};
913 my $hold = Koha::Holds->find($reserve_id);
915 # get the priority on this record....
916 my $priority = $hold->priority;
918 # update the hold statuses, no need to store it though, we will be deleting it anyway
919 $hold->set(
921 found => 'F',
922 priority => 0,
926 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
927 Koha::Old::Hold->new( $hold->unblessed() )->store();
929 $hold->delete();
931 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
932 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
933 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
936 # now fix the priority on the others (if the priority wasn't
937 # already sorted!)....
938 unless ( $priority == 0 ) {
939 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
943 =head2 ModReserveStatus
945 &ModReserveStatus($itemnumber, $newstatus);
947 Update the reserve status for the active (priority=0) reserve.
949 $itemnumber is the itemnumber the reserve is on
951 $newstatus is the new status.
953 =cut
955 sub ModReserveStatus {
957 #first : check if we have a reservation for this item .
958 my ($itemnumber, $newstatus) = @_;
959 my $dbh = C4::Context->dbh;
961 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
962 my $sth_set = $dbh->prepare($query);
963 $sth_set->execute( $newstatus, $itemnumber );
965 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
966 CartToShelf( $itemnumber );
970 =head2 ModReserveAffect
972 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
974 This function affect an item and a status for a given reserve, either fetched directly
975 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
976 is given, only first reserve returned is affected, which is ok for anything but
977 multi-item holds.
979 if $transferToDo is not set, then the status is set to "Waiting" as well.
980 otherwise, a transfer is on the way, and the end of the transfer will
981 take care of the waiting status
983 =cut
985 sub ModReserveAffect {
986 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
987 my $dbh = C4::Context->dbh;
989 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
990 # attached to $itemnumber
991 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
992 $sth->execute($itemnumber);
993 my ($biblionumber) = $sth->fetchrow;
995 # get request - need to find out if item is already
996 # waiting in order to not send duplicate hold filled notifications
998 my $hold;
999 # Find hold by id if we have it
1000 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1001 # Find item level hold for this item if there is one
1002 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1003 # Find record level hold if there is no item level hold
1004 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1006 return unless $hold;
1008 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1010 $hold->itemnumber($itemnumber);
1011 $hold->set_waiting($transferToDo);
1013 _koha_notify_reserve( $hold->reserve_id )
1014 if ( !$transferToDo && !$already_on_shelf );
1016 _FixPriority( { biblionumber => $biblionumber } );
1018 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1019 CartToShelf($itemnumber);
1022 return;
1025 =head2 ModReserveCancelAll
1027 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1029 function to cancel reserv,check other reserves, and transfer document if it's necessary
1031 =cut
1033 sub ModReserveCancelAll {
1034 my $messages;
1035 my $nextreservinfo;
1036 my ( $itemnumber, $borrowernumber ) = @_;
1038 #step 1 : cancel the reservation
1039 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1040 return unless $holds->count;
1041 $holds->next->cancel;
1043 #step 2 launch the subroutine of the others reserves
1044 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1046 return ( $messages, $nextreservinfo );
1049 =head2 ModReserveMinusPriority
1051 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1053 Reduce the values of queued list
1055 =cut
1057 sub ModReserveMinusPriority {
1058 my ( $itemnumber, $reserve_id ) = @_;
1060 #first step update the value of the first person on reserv
1061 my $dbh = C4::Context->dbh;
1062 my $query = "
1063 UPDATE reserves
1064 SET priority = 0 , itemnumber = ?
1065 WHERE reserve_id = ?
1067 my $sth_upd = $dbh->prepare($query);
1068 $sth_upd->execute( $itemnumber, $reserve_id );
1069 # second step update all others reserves
1070 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1073 =head2 IsAvailableForItemLevelRequest
1075 my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1077 Checks whether a given item record is available for an
1078 item-level hold request. An item is available if
1080 * it is not lost AND
1081 * it is not damaged AND
1082 * it is not withdrawn AND
1083 * does not have a not for loan value > 0
1085 Need to check the issuingrules onshelfholds column,
1086 if this is set items on the shelf can be placed on hold
1088 Note that IsAvailableForItemLevelRequest() does not
1089 check if the staff operator is authorized to place
1090 a request on the item - in particular,
1091 this routine does not check IndependentBranches
1092 and canreservefromotherbranches.
1094 =cut
1096 sub IsAvailableForItemLevelRequest {
1097 my $item = shift;
1098 my $borrower = shift;
1100 my $dbh = C4::Context->dbh;
1101 # must check the notforloan setting of the itemtype
1102 # FIXME - a lot of places in the code do this
1103 # or something similar - need to be
1104 # consolidated
1105 my $itype = _get_itype($item);
1106 my $notforloan_per_itemtype
1107 = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1108 undef, $itype);
1110 return 0 if
1111 $notforloan_per_itemtype ||
1112 $item->{itemlost} ||
1113 $item->{notforloan} > 0 ||
1114 $item->{withdrawn} ||
1115 ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1117 my $on_shelf_holds = _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1119 if ( $on_shelf_holds == 1 ) {
1120 return 1;
1121 } elsif ( $on_shelf_holds == 2 ) {
1122 my @items =
1123 Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1125 my $any_available = 0;
1127 foreach my $i (@items) {
1129 my $circ_control_branch = C4::Circulation::_GetCircControlBranch( $i->unblessed(), $borrower );
1130 my $branchitemrule = C4::Circulation::GetBranchItemRule( $circ_control_branch, $i->itype );
1132 $any_available = 1
1133 unless $i->itemlost
1134 || $i->notforloan > 0
1135 || $i->withdrawn
1136 || $i->onloan
1137 || IsItemOnHoldAndFound( $i->id )
1138 || ( $i->damaged
1139 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1140 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1141 || $branchitemrule->{holdallowed} == 1 && $borrower->{branchcode} ne $i->homebranch;
1144 return $any_available ? 0 : 1;
1147 return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting";
1150 =head2 OnShelfHoldsAllowed
1152 OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode);
1154 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf
1155 holds are allowed, returns true if so.
1157 =cut
1159 sub OnShelfHoldsAllowed {
1160 my ($item, $borrower) = @_;
1162 my $itype = _get_itype($item);
1163 return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1166 sub _get_itype {
1167 my $item = shift;
1169 my $itype;
1170 if (C4::Context->preference('item-level_itypes')) {
1171 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1172 # When GetItem is fixed, we can remove this
1173 $itype = $item->{itype};
1175 else {
1176 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1177 # So if we already have a biblioitems join when calling this function,
1178 # we don't need to access the database again
1179 $itype = $item->{itemtype};
1181 unless ($itype) {
1182 my $dbh = C4::Context->dbh;
1183 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1184 my $sth = $dbh->prepare($query);
1185 $sth->execute($item->{biblioitemnumber});
1186 if (my $data = $sth->fetchrow_hashref()){
1187 $itype = $data->{itemtype};
1190 return $itype;
1193 sub _OnShelfHoldsAllowed {
1194 my ($itype,$borrowercategory,$branchcode) = @_;
1196 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ categorycode => $borrowercategory, itemtype => $itype, branchcode => $branchcode });
1197 return $issuing_rule ? $issuing_rule->onshelfholds : undef;
1200 =head2 AlterPriority
1202 AlterPriority( $where, $reserve_id );
1204 This function changes a reserve's priority up, down, to the top, or to the bottom.
1205 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1207 =cut
1209 sub AlterPriority {
1210 my ( $where, $reserve_id ) = @_;
1212 my $hold = Koha::Holds->find( $reserve_id );
1213 return unless $hold;
1215 if ( $hold->cancellationdate ) {
1216 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1217 return;
1220 if ( $where eq 'up' || $where eq 'down' ) {
1222 my $priority = $hold->priority;
1223 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1224 _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1226 } elsif ( $where eq 'top' ) {
1228 _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1230 } elsif ( $where eq 'bottom' ) {
1232 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1235 # FIXME Should return the new priority
1238 =head2 ToggleLowestPriority
1240 ToggleLowestPriority( $borrowernumber, $biblionumber );
1242 This function sets the lowestPriority field to true if is false, and false if it is true.
1244 =cut
1246 sub ToggleLowestPriority {
1247 my ( $reserve_id ) = @_;
1249 my $dbh = C4::Context->dbh;
1251 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1252 $sth->execute( $reserve_id );
1254 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1257 =head2 ToggleSuspend
1259 ToggleSuspend( $reserve_id );
1261 This function sets the suspend field to true if is false, and false if it is true.
1262 If the reserve is currently suspended with a suspend_until date, that date will
1263 be cleared when it is unsuspended.
1265 =cut
1267 sub ToggleSuspend {
1268 my ( $reserve_id, $suspend_until ) = @_;
1270 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1272 my $hold = Koha::Holds->find( $reserve_id );
1274 if ( $hold->is_suspended ) {
1275 $hold->resume()
1276 } else {
1277 $hold->suspend_hold( $suspend_until );
1281 =head2 SuspendAll
1283 SuspendAll(
1284 borrowernumber => $borrowernumber,
1285 [ biblionumber => $biblionumber, ]
1286 [ suspend_until => $suspend_until, ]
1287 [ suspend => $suspend ]
1290 This function accepts a set of hash keys as its parameters.
1291 It requires either borrowernumber or biblionumber, or both.
1293 suspend_until is wholly optional.
1295 =cut
1297 sub SuspendAll {
1298 my %params = @_;
1300 my $borrowernumber = $params{'borrowernumber'} || undef;
1301 my $biblionumber = $params{'biblionumber'} || undef;
1302 my $suspend_until = $params{'suspend_until'} || undef;
1303 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1305 $suspend_until = eval { dt_from_string($suspend_until) }
1306 if ( defined($suspend_until) );
1308 return unless ( $borrowernumber || $biblionumber );
1310 my $params;
1311 $params->{found} = undef;
1312 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1313 $params->{biblionumber} = $biblionumber if $biblionumber;
1315 my @holds = Koha::Holds->search($params);
1317 if ($suspend) {
1318 map { $_->suspend_hold($suspend_until) } @holds;
1320 else {
1321 map { $_->resume() } @holds;
1326 =head2 _FixPriority
1328 _FixPriority({
1329 reserve_id => $reserve_id,
1330 [rank => $rank,]
1331 [ignoreSetLowestRank => $ignoreSetLowestRank]
1336 _FixPriority({ biblionumber => $biblionumber});
1338 This routine adjusts the priority of a hold request and holds
1339 on the same bib.
1341 In the first form, where a reserve_id is passed, the priority of the
1342 hold is set to supplied rank, and other holds for that bib are adjusted
1343 accordingly. If the rank is "del", the hold is cancelled. If no rank
1344 is supplied, all of the holds on that bib have their priority adjusted
1345 as if the second form had been used.
1347 In the second form, where a biblionumber is passed, the holds on that
1348 bib (that are not captured) are sorted in order of increasing priority,
1349 then have reserves.priority set so that the first non-captured hold
1350 has its priority set to 1, the second non-captured hold has its priority
1351 set to 2, and so forth.
1353 In both cases, holds that have the lowestPriority flag on are have their
1354 priority adjusted to ensure that they remain at the end of the line.
1356 Note that the ignoreSetLowestRank parameter is meant to be used only
1357 when _FixPriority calls itself.
1359 =cut
1361 sub _FixPriority {
1362 my ( $params ) = @_;
1363 my $reserve_id = $params->{reserve_id};
1364 my $rank = $params->{rank} // '';
1365 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1366 my $biblionumber = $params->{biblionumber};
1368 my $dbh = C4::Context->dbh;
1370 my $hold;
1371 if ( $reserve_id ) {
1372 $hold = Koha::Holds->find( $reserve_id );
1373 return unless $hold;
1376 unless ( $biblionumber ) { # FIXME This is a very weird API
1377 $biblionumber = $hold->biblionumber;
1380 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1381 $hold->cancel;
1383 elsif ( $rank eq "W" || $rank eq "0" ) {
1385 # make sure priority for waiting or in-transit items is 0
1386 my $query = "
1387 UPDATE reserves
1388 SET priority = 0
1389 WHERE reserve_id = ?
1390 AND found IN ('W', 'T')
1392 my $sth = $dbh->prepare($query);
1393 $sth->execute( $reserve_id );
1395 my @priority;
1397 # get whats left
1398 my $query = "
1399 SELECT reserve_id, borrowernumber, reservedate
1400 FROM reserves
1401 WHERE biblionumber = ?
1402 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1403 ORDER BY priority ASC
1405 my $sth = $dbh->prepare($query);
1406 $sth->execute( $biblionumber );
1407 while ( my $line = $sth->fetchrow_hashref ) {
1408 push( @priority, $line );
1411 # To find the matching index
1412 my $i;
1413 my $key = -1; # to allow for 0 to be a valid result
1414 for ( $i = 0 ; $i < @priority ; $i++ ) {
1415 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1416 $key = $i; # save the index
1417 last;
1421 # if index exists in array then move it to new position
1422 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1423 my $new_rank = $rank -
1424 1; # $new_rank is what you want the new index to be in the array
1425 my $moving_item = splice( @priority, $key, 1 );
1426 splice( @priority, $new_rank, 0, $moving_item );
1429 # now fix the priority on those that are left....
1430 $query = "
1431 UPDATE reserves
1432 SET priority = ?
1433 WHERE reserve_id = ?
1435 $sth = $dbh->prepare($query);
1436 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1437 $sth->execute(
1438 $j + 1,
1439 $priority[$j]->{'reserve_id'}
1443 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1444 $sth->execute();
1446 unless ( $ignoreSetLowestRank ) {
1447 while ( my $res = $sth->fetchrow_hashref() ) {
1448 _FixPriority({
1449 reserve_id => $res->{'reserve_id'},
1450 rank => '999999',
1451 ignoreSetLowestRank => 1
1457 =head2 _Findgroupreserve
1459 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1461 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1462 first match found. If neither, then we look for non-holds-queue based holds.
1463 Lookahead is the number of days to look in advance.
1465 C<&_Findgroupreserve> returns :
1466 C<@results> is an array of references-to-hash whose keys are mostly
1467 fields from the reserves table of the Koha database, plus
1468 C<biblioitemnumber>.
1470 =cut
1472 sub _Findgroupreserve {
1473 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1474 my $dbh = C4::Context->dbh;
1476 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1477 # check for exact targeted match
1478 my $item_level_target_query = qq{
1479 SELECT reserves.biblionumber AS biblionumber,
1480 reserves.borrowernumber AS borrowernumber,
1481 reserves.reservedate AS reservedate,
1482 reserves.branchcode AS branchcode,
1483 reserves.cancellationdate AS cancellationdate,
1484 reserves.found AS found,
1485 reserves.reservenotes AS reservenotes,
1486 reserves.priority AS priority,
1487 reserves.timestamp AS timestamp,
1488 biblioitems.biblioitemnumber AS biblioitemnumber,
1489 reserves.itemnumber AS itemnumber,
1490 reserves.reserve_id AS reserve_id,
1491 reserves.itemtype AS itemtype
1492 FROM reserves
1493 JOIN biblioitems USING (biblionumber)
1494 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1495 WHERE found IS NULL
1496 AND priority > 0
1497 AND item_level_request = 1
1498 AND itemnumber = ?
1499 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1500 AND suspend = 0
1501 ORDER BY priority
1503 my $sth = $dbh->prepare($item_level_target_query);
1504 $sth->execute($itemnumber, $lookahead||0);
1505 my @results;
1506 if ( my $data = $sth->fetchrow_hashref ) {
1507 push( @results, $data )
1508 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1510 return @results if @results;
1512 # check for title-level targeted match
1513 my $title_level_target_query = qq{
1514 SELECT reserves.biblionumber AS biblionumber,
1515 reserves.borrowernumber AS borrowernumber,
1516 reserves.reservedate AS reservedate,
1517 reserves.branchcode AS branchcode,
1518 reserves.cancellationdate AS cancellationdate,
1519 reserves.found AS found,
1520 reserves.reservenotes AS reservenotes,
1521 reserves.priority AS priority,
1522 reserves.timestamp AS timestamp,
1523 biblioitems.biblioitemnumber AS biblioitemnumber,
1524 reserves.itemnumber AS itemnumber,
1525 reserves.reserve_id AS reserve_id,
1526 reserves.itemtype AS itemtype
1527 FROM reserves
1528 JOIN biblioitems USING (biblionumber)
1529 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1530 WHERE found IS NULL
1531 AND priority > 0
1532 AND item_level_request = 0
1533 AND hold_fill_targets.itemnumber = ?
1534 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1535 AND suspend = 0
1536 ORDER BY priority
1538 $sth = $dbh->prepare($title_level_target_query);
1539 $sth->execute($itemnumber, $lookahead||0);
1540 @results = ();
1541 if ( my $data = $sth->fetchrow_hashref ) {
1542 push( @results, $data )
1543 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1545 return @results if @results;
1547 my $query = qq{
1548 SELECT reserves.biblionumber AS biblionumber,
1549 reserves.borrowernumber AS borrowernumber,
1550 reserves.reservedate AS reservedate,
1551 reserves.waitingdate AS waitingdate,
1552 reserves.branchcode AS branchcode,
1553 reserves.cancellationdate AS cancellationdate,
1554 reserves.found AS found,
1555 reserves.reservenotes AS reservenotes,
1556 reserves.priority AS priority,
1557 reserves.timestamp AS timestamp,
1558 reserves.itemnumber AS itemnumber,
1559 reserves.reserve_id AS reserve_id,
1560 reserves.itemtype AS itemtype
1561 FROM reserves
1562 WHERE reserves.biblionumber = ?
1563 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1564 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1565 AND suspend = 0
1566 ORDER BY priority
1568 $sth = $dbh->prepare($query);
1569 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1570 @results = ();
1571 while ( my $data = $sth->fetchrow_hashref ) {
1572 push( @results, $data )
1573 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1575 return @results;
1578 =head2 _koha_notify_reserve
1580 _koha_notify_reserve( $hold->reserve_id );
1582 Sends a notification to the patron that their hold has been filled (through
1583 ModReserveAffect, _not_ ModReserveFill)
1585 The letter code for this notice may be found using the following query:
1587 select distinct letter_code
1588 from message_transports
1589 inner join message_attributes using (message_attribute_id)
1590 where message_name = 'Hold_Filled'
1592 This will probably sipmly be 'HOLD', but because it is defined in the database,
1593 it is subject to addition or change.
1595 The following tables are availalbe witin the notice:
1597 branches
1598 borrowers
1599 biblio
1600 biblioitems
1601 reserves
1602 items
1604 =cut
1606 sub _koha_notify_reserve {
1607 my $reserve_id = shift;
1608 my $hold = Koha::Holds->find($reserve_id);
1609 my $borrowernumber = $hold->borrowernumber;
1611 my $patron = Koha::Patrons->find( $borrowernumber );
1613 # Try to get the borrower's email address
1614 my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1616 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1617 borrowernumber => $borrowernumber,
1618 message_name => 'Hold_Filled'
1619 } );
1621 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1623 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1625 my %letter_params = (
1626 module => 'reserves',
1627 branchcode => $hold->branchcode,
1628 lang => $patron->lang,
1629 tables => {
1630 'branches' => $library,
1631 'borrowers' => $patron->unblessed,
1632 'biblio' => $hold->biblionumber,
1633 'biblioitems' => $hold->biblionumber,
1634 'reserves' => $hold->unblessed,
1635 'items' => $hold->itemnumber,
1639 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.
1640 my $send_notification = sub {
1641 my ( $mtt, $letter_code ) = (@_);
1642 return unless defined $letter_code;
1643 $letter_params{letter_code} = $letter_code;
1644 $letter_params{message_transport_type} = $mtt;
1645 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1646 unless ($letter) {
1647 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1648 return;
1651 C4::Letters::EnqueueLetter( {
1652 letter => $letter,
1653 borrowernumber => $borrowernumber,
1654 from_address => $admin_email_address,
1655 message_transport_type => $mtt,
1656 } );
1659 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1660 next if (
1661 ( $mtt eq 'email' and not $to_address ) # No email address
1662 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1663 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1666 &$send_notification($mtt, $letter_code);
1667 $notification_sent++;
1669 #Making sure that a print notification is sent if no other transport types can be utilized.
1670 if (! $notification_sent) {
1671 &$send_notification('print', 'HOLD');
1676 =head2 _ShiftPriorityByDateAndPriority
1678 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1680 This increments the priority of all reserves after the one
1681 with either the lowest date after C<$reservedate>
1682 or the lowest priority after C<$priority>.
1684 It effectively makes room for a new reserve to be inserted with a certain
1685 priority, which is returned.
1687 This is most useful when the reservedate can be set by the user. It allows
1688 the new reserve to be placed before other reserves that have a later
1689 reservedate. Since priority also is set by the form in reserves/request.pl
1690 the sub accounts for that too.
1692 =cut
1694 sub _ShiftPriorityByDateAndPriority {
1695 my ( $biblio, $resdate, $new_priority ) = @_;
1697 my $dbh = C4::Context->dbh;
1698 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1699 my $sth = $dbh->prepare( $query );
1700 $sth->execute( $biblio, $resdate, $new_priority );
1701 my $min_priority = $sth->fetchrow;
1702 # if no such matches are found, $new_priority remains as original value
1703 $new_priority = $min_priority if ( $min_priority );
1705 # Shift the priority up by one; works in conjunction with the next SQL statement
1706 $query = "UPDATE reserves
1707 SET priority = priority+1
1708 WHERE biblionumber = ?
1709 AND borrowernumber = ?
1710 AND reservedate = ?
1711 AND found IS NULL";
1712 my $sth_update = $dbh->prepare( $query );
1714 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1715 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1716 $sth = $dbh->prepare( $query );
1717 $sth->execute( $new_priority, $biblio );
1718 while ( my $row = $sth->fetchrow_hashref ) {
1719 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1722 return $new_priority; # so the caller knows what priority they wind up receiving
1725 =head2 OPACItemHoldsAllowed
1727 OPACItemHoldsAllowed($item_record,$borrower_record);
1729 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see
1730 if specific item holds are allowed, returns true if so.
1732 =cut
1734 sub OPACItemHoldsAllowed {
1735 my ($item,$borrower) = @_;
1737 my $branchcode = $item->{homebranch} or die "No homebranch";
1738 my $itype;
1739 my $dbh = C4::Context->dbh;
1740 if (C4::Context->preference('item-level_itypes')) {
1741 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1742 # When GetItem is fixed, we can remove this
1743 $itype = $item->{itype};
1745 else {
1746 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1747 my $sth = $dbh->prepare($query);
1748 $sth->execute($item->{biblioitemnumber});
1749 if (my $data = $sth->fetchrow_hashref()){
1750 $itype = $data->{itemtype};
1754 my $query = "SELECT opacitemholds,categorycode,itemtype,branchcode FROM issuingrules WHERE
1755 (issuingrules.categorycode = ? OR issuingrules.categorycode = '*')
1757 (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
1759 (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')
1760 ORDER BY
1761 issuingrules.categorycode desc,
1762 issuingrules.itemtype desc,
1763 issuingrules.branchcode desc
1764 LIMIT 1";
1765 my $sth = $dbh->prepare($query);
1766 $sth->execute($borrower->{categorycode},$itype,$branchcode);
1767 my $data = $sth->fetchrow_hashref;
1768 my $opacitemholds = uc substr ($data->{opacitemholds}, 0, 1);
1769 return '' if $opacitemholds eq 'N';
1770 return $opacitemholds;
1773 =head2 MoveReserve
1775 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1777 Use when checking out an item to handle reserves
1778 If $cancelreserve boolean is set to true, it will remove existing reserve
1780 =cut
1782 sub MoveReserve {
1783 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1785 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1786 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
1787 return unless $res;
1789 my $biblionumber = $res->{biblionumber};
1791 if ($res->{borrowernumber} == $borrowernumber) {
1792 ModReserveFill($res);
1794 else {
1795 # warn "Reserved";
1796 # The item is reserved by someone else.
1797 # Find this item in the reserves
1799 my $borr_res;
1800 foreach (@$all_reserves) {
1801 $_->{'borrowernumber'} == $borrowernumber or next;
1802 $_->{'biblionumber'} == $biblionumber or next;
1804 $borr_res = $_;
1805 last;
1808 if ( $borr_res ) {
1809 # The item is reserved by the current patron
1810 ModReserveFill($borr_res);
1813 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1814 RevertWaitingStatus({ itemnumber => $itemnumber });
1816 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1817 my $hold = Koha::Holds->find( $res->{reserve_id} );
1818 $hold->cancel;
1823 =head2 MergeHolds
1825 MergeHolds($dbh,$to_biblio, $from_biblio);
1827 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1829 =cut
1831 sub MergeHolds {
1832 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1833 my $sth = $dbh->prepare(
1834 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1836 $sth->execute($from_biblio);
1837 if ( my $data = $sth->fetchrow_hashref() ) {
1839 # holds exist on old record, if not we don't need to do anything
1840 $sth = $dbh->prepare(
1841 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1842 $sth->execute( $to_biblio, $from_biblio );
1844 # Reorder by date
1845 # don't reorder those already waiting
1847 $sth = $dbh->prepare(
1848 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1850 my $upd_sth = $dbh->prepare(
1851 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1852 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1854 $sth->execute( $to_biblio, 'W', 'T' );
1855 my $priority = 1;
1856 while ( my $reserve = $sth->fetchrow_hashref() ) {
1857 $upd_sth->execute(
1858 $priority, $to_biblio,
1859 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1860 $reserve->{'itemnumber'}
1862 $priority++;
1867 =head2 RevertWaitingStatus
1869 RevertWaitingStatus({ itemnumber => $itemnumber });
1871 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1873 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1874 item level hold, even if it was only a bibliolevel hold to
1875 begin with. This is because we can no longer know if a hold
1876 was item-level or bib-level after a hold has been set to
1877 waiting status.
1879 =cut
1881 sub RevertWaitingStatus {
1882 my ( $params ) = @_;
1883 my $itemnumber = $params->{'itemnumber'};
1885 return unless ( $itemnumber );
1887 my $dbh = C4::Context->dbh;
1889 ## Get the waiting reserve we want to revert
1890 my $query = "
1891 SELECT * FROM reserves
1892 WHERE itemnumber = ?
1893 AND found IS NOT NULL
1895 my $sth = $dbh->prepare( $query );
1896 $sth->execute( $itemnumber );
1897 my $reserve = $sth->fetchrow_hashref();
1899 ## Increment the priority of all other non-waiting
1900 ## reserves for this bib record
1901 $query = "
1902 UPDATE reserves
1904 priority = priority + 1
1905 WHERE
1906 biblionumber = ?
1908 priority > 0
1910 $sth = $dbh->prepare( $query );
1911 $sth->execute( $reserve->{'biblionumber'} );
1913 ## Fix up the currently waiting reserve
1914 $query = "
1915 UPDATE reserves
1917 priority = 1,
1918 found = NULL,
1919 waitingdate = NULL
1920 WHERE
1921 reserve_id = ?
1923 $sth = $dbh->prepare( $query );
1924 $sth->execute( $reserve->{'reserve_id'} );
1925 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1928 =head2 ReserveSlip
1930 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
1932 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1934 The letter code will be HOLD_SLIP, and the following tables are
1935 available within the slip:
1937 reserves
1938 branches
1939 borrowers
1940 biblio
1941 biblioitems
1942 items
1944 =cut
1946 sub ReserveSlip {
1947 my ($branch, $borrowernumber, $biblionumber) = @_;
1949 # return unless ( C4::Context->boolean_preference('printreserveslips') );
1950 my $patron = Koha::Patrons->find( $borrowernumber );
1952 my $hold = Koha::Holds->search({biblionumber => $biblionumber, borrowernumber => $borrowernumber })->next;
1953 return unless $hold;
1954 my $reserve = $hold->unblessed;
1956 return C4::Letters::GetPreparedLetter (
1957 module => 'circulation',
1958 letter_code => 'HOLD_SLIP',
1959 branchcode => $branch,
1960 lang => $patron->lang,
1961 tables => {
1962 'reserves' => $reserve,
1963 'branches' => $reserve->{branchcode},
1964 'borrowers' => $reserve->{borrowernumber},
1965 'biblio' => $reserve->{biblionumber},
1966 'biblioitems' => $reserve->{biblionumber},
1967 'items' => $reserve->{itemnumber},
1972 =head2 GetReservesControlBranch
1974 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
1976 Return the branchcode to be used to determine which reserves
1977 policy applies to a transaction.
1979 C<$item> is a hashref for an item. Only 'homebranch' is used.
1981 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
1983 =cut
1985 sub GetReservesControlBranch {
1986 my ( $item, $borrower ) = @_;
1988 my $reserves_control = C4::Context->preference('ReservesControlBranch');
1990 my $branchcode =
1991 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
1992 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
1993 : undef;
1995 return $branchcode;
1998 =head2 CalculatePriority
2000 my $p = CalculatePriority($biblionumber, $resdate);
2002 Calculate priority for a new reserve on biblionumber, placing it at
2003 the end of the line of all holds whose start date falls before
2004 the current system time and that are neither on the hold shelf
2005 or in transit.
2007 The reserve date parameter is optional; if it is supplied, the
2008 priority is based on the set of holds whose start date falls before
2009 the parameter value.
2011 After calculation of this priority, it is recommended to call
2012 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2013 AddReserves.
2015 =cut
2017 sub CalculatePriority {
2018 my ( $biblionumber, $resdate ) = @_;
2020 my $sql = q{
2021 SELECT COUNT(*) FROM reserves
2022 WHERE biblionumber = ?
2023 AND priority > 0
2024 AND (found IS NULL OR found = '')
2026 #skip found==W or found==T (waiting or transit holds)
2027 if( $resdate ) {
2028 $sql.= ' AND ( reservedate <= ? )';
2030 else {
2031 $sql.= ' AND ( reservedate < NOW() )';
2033 my $dbh = C4::Context->dbh();
2034 my @row = $dbh->selectrow_array(
2035 $sql,
2036 undef,
2037 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2040 return @row ? $row[0]+1 : 1;
2043 =head2 IsItemOnHoldAndFound
2045 my $bool = IsItemFoundHold( $itemnumber );
2047 Returns true if the item is currently on hold
2048 and that hold has a non-null found status ( W, T, etc. )
2050 =cut
2052 sub IsItemOnHoldAndFound {
2053 my ($itemnumber) = @_;
2055 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2057 my $found = $rs->count(
2059 itemnumber => $itemnumber,
2060 found => { '!=' => undef }
2064 return $found;
2067 =head2 GetMaxPatronHoldsForRecord
2069 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2071 For multiple holds on a given record for a given patron, the max
2072 number of record level holds that a patron can be placed is the highest
2073 value of the holds_per_record rule for each item if the record for that
2074 patron. This subroutine finds and returns the highest holds_per_record
2075 rule value for a given patron id and record id.
2077 =cut
2079 sub GetMaxPatronHoldsForRecord {
2080 my ( $borrowernumber, $biblionumber ) = @_;
2082 my $patron = Koha::Patrons->find($borrowernumber);
2083 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2085 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2087 my $categorycode = $patron->categorycode;
2088 my $branchcode;
2089 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2091 my $max = 0;
2092 foreach my $item (@items) {
2093 my $itemtype = $item->effective_itemtype();
2095 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2097 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2098 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2099 $max = $holds_per_record if $holds_per_record > $max;
2102 return $max;
2105 =head2 GetHoldRule
2107 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2109 Returns the matching hold related issuingrule fields for a given
2110 patron category, itemtype, and library.
2112 =cut
2114 sub GetHoldRule {
2115 my ( $categorycode, $itemtype, $branchcode ) = @_;
2117 my $dbh = C4::Context->dbh;
2119 my $sth = $dbh->prepare(
2121 SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record
2122 FROM issuingrules
2123 WHERE (categorycode in (?,'*') )
2124 AND (itemtype IN (?,'*'))
2125 AND (branchcode IN (?,'*'))
2126 ORDER BY categorycode DESC,
2127 itemtype DESC,
2128 branchcode DESC
2132 $sth->execute( $categorycode, $itemtype, $branchcode );
2134 return $sth->fetchrow_hashref();
2137 =head1 AUTHOR
2139 Koha Development Team <http://koha-community.org/>
2141 =cut