Bug 7534: (RM follow-up) Fix tests and adjust after bug 15524
[koha.git] / C4 / Reserves.pm
blob213c7c376d19f71778f8296a4a8784f3cc23fa22
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 &AlterPriority
130 &ToggleLowestPriority
132 &ReserveSlip
133 &ToggleSuspend
134 &SuspendAll
136 &GetReservesControlBranch
138 IsItemOnHoldAndFound
140 GetMaxPatronHoldsForRecord
142 @EXPORT_OK = qw( MergeHolds );
145 =head2 AddReserve
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:
153 branches
154 borrowers
155 biblio
156 biblioitems
157 items
158 reserves
160 =cut
162 sub AddReserve {
163 my (
164 $branch, $borrowernumber, $biblionumber, $bibitems,
165 $priority, $resdate, $expdate, $notes,
166 $title, $checkitem, $found, $itemtype
167 ) = @_;
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') ) {
177 $priority = 0;
178 my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
179 if ( $item->holdingbranch eq $branch ) {
180 $found = 'W';
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 );
190 my $waitingdate;
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,
210 found => $found,
211 waitingdate => $waitingdate,
212 expirationdate => $expdate,
213 itemtype => $itemtype,
215 )->store();
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,
240 tables => {
241 'branches' => $library->unblessed,
242 'borrowers' => $patron->unblessed,
243 'biblio' => $biblionumber,
244 'biblioitems' => $biblionumber,
245 'items' => $checkitem,
246 'reserves' => $hold->unblessed,
248 ) ) {
250 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
252 C4::Letters::EnqueueLetter(
253 { letter => $letter,
254 borrowernumber => $borrowernumber,
255 message_transport_type => 'email',
256 from_address => $admin_email_address,
257 to_address => $admin_email_address,
263 return $reserve_id;
266 =head2 CanBookBeReserved
268 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
269 if ($canReserve eq 'OK') { #We can reserve this Item! }
271 See CanItemBeReserved() for possible return values.
273 =cut
275 sub CanBookBeReserved{
276 my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
278 my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
279 #get items linked via host records
280 my @hostitems = get_hostitemnumbers_of($biblionumber);
281 if (@hostitems){
282 push (@itemnumbers, @hostitems);
285 my $canReserve;
286 foreach my $itemnumber (@itemnumbers) {
287 $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
288 return { status => 'OK' } if $canReserve->{status} eq 'OK';
290 return $canReserve;
293 =head2 CanItemBeReserved
295 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
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
304 { status => libraryNotFound }, if given branchcode is not an existing library
305 { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location
307 =cut
309 sub CanItemBeReserved {
310 my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
312 my $dbh = C4::Context->dbh;
313 my $ruleitemtype; # itemtype of the matching issuing rule
314 my $allowedreserves = 0; # Total number of holds allowed across all records
315 my $holds_per_record = 1; # Total number of holds allowed for this one given record
317 # we retrieve borrowers and items informations #
318 # item->{itype} will come for biblioitems if necessery
319 my $item = GetItem($itemnumber);
320 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
321 my $patron = Koha::Patrons->find( $borrowernumber );
322 my $borrower = $patron->unblessed;
324 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
325 return { status =>'damaged' }
326 if ( $item->{damaged}
327 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
329 # Check for the age restriction
330 my ( $ageRestriction, $daysToAgeRestriction ) =
331 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
332 return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
334 # Check that the patron doesn't have an item level hold on this item already
335 return { status =>'itemAlreadyOnHold' }
336 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
338 my $controlbranch = C4::Context->preference('ReservesControlBranch');
340 my $querycount = q{
341 SELECT count(*) AS count
342 FROM reserves
343 LEFT JOIN items USING (itemnumber)
344 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
345 LEFT JOIN borrowers USING (borrowernumber)
346 WHERE borrowernumber = ?
349 my $branchcode = "";
350 my $branchfield = "reserves.branchcode";
352 if ( $controlbranch eq "ItemHomeLibrary" ) {
353 $branchfield = "items.homebranch";
354 $branchcode = $item->{homebranch};
356 elsif ( $controlbranch eq "PatronLibrary" ) {
357 $branchfield = "borrowers.branchcode";
358 $branchcode = $borrower->{branchcode};
361 # we retrieve rights
362 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) {
363 $ruleitemtype = $rights->{itemtype};
364 $allowedreserves = $rights->{reservesallowed};
365 $holds_per_record = $rights->{holds_per_record};
367 else {
368 $ruleitemtype = '*';
371 $item = Koha::Items->find( $itemnumber );
372 my $holds = Koha::Holds->search(
374 borrowernumber => $borrowernumber,
375 biblionumber => $item->biblionumber,
376 found => undef, # Found holds don't count against a patron's holds limit
379 if ( $holds->count() >= $holds_per_record ) {
380 return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
383 # we retrieve count
385 $querycount .= "AND $branchfield = ?";
387 # If using item-level itypes, fall back to the record
388 # level itemtype if the hold has no associated item
389 $querycount .=
390 C4::Context->preference('item-level_itypes')
391 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
392 : " AND biblioitems.itemtype = ?"
393 if ( $ruleitemtype ne "*" );
395 my $sthcount = $dbh->prepare($querycount);
397 if ( $ruleitemtype eq "*" ) {
398 $sthcount->execute( $borrowernumber, $branchcode );
400 else {
401 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
404 my $reservecount = "0";
405 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
406 $reservecount = $rowcount->{count};
409 # we check if it's ok or not
410 if ( $reservecount >= $allowedreserves ) {
411 return { status => 'tooManyReserves', limit => $allowedreserves };
414 # Now we need to check hold limits by patron category
415 my $schema = Koha::Database->new()->schema();
416 my $rule = $schema->resultset('BranchBorrowerCircRule')->find(
418 branchcode => $branchcode,
419 categorycode => $borrower->{categorycode},
422 $rule ||= $schema->resultset('DefaultBorrowerCircRule')->find(
424 categorycode => $borrower->{categorycode}
427 if ( $rule && defined $rule->max_holds ) {
428 my $total_holds_count = Koha::Holds->search(
430 borrowernumber => $borrower->{borrowernumber}
432 )->count();
434 return { status => 'tooManyReserves', limit => $rule->max_holds } if $total_holds_count >= $rule->max_holds;
437 my $circ_control_branch =
438 C4::Circulation::_GetCircControlBranch( $item->unblessed(), $borrower );
439 my $branchitemrule =
440 C4::Circulation::GetBranchItemRule( $circ_control_branch, $item->itype );
442 if ( $branchitemrule->{holdallowed} == 0 ) {
443 return { status => 'notReservable' };
446 if ( $branchitemrule->{holdallowed} == 1
447 && $borrower->{branchcode} ne $item->homebranch )
449 return { status => 'cannotReserveFromOtherBranches' };
452 # If reservecount is ok, we check item branch if IndependentBranches is ON
453 # and canreservefromotherbranches is OFF
454 if ( C4::Context->preference('IndependentBranches')
455 and !C4::Context->preference('canreservefromotherbranches') )
457 my $itembranch = $item->homebranch;
458 if ( $itembranch ne $borrower->{branchcode} ) {
459 return { status => 'cannotReserveFromOtherBranches' };
463 if ($pickup_branchcode) {
464 my $destination = Koha::Libraries->find({
465 branchcode => $pickup_branchcode,
467 unless ($destination) {
468 return { status => 'libraryNotFound' };
470 unless ($destination->pickup_location) {
471 return { status => 'libraryNotPickupLocation' };
475 return { status => 'OK' };
478 =head2 CanReserveBeCanceledFromOpac
480 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
482 returns 1 if reserve can be cancelled by user from OPAC.
483 First check if reserve belongs to user, next checks if reserve is not in
484 transfer or waiting status
486 =cut
488 sub CanReserveBeCanceledFromOpac {
489 my ($reserve_id, $borrowernumber) = @_;
491 return unless $reserve_id and $borrowernumber;
492 my $reserve = Koha::Holds->find($reserve_id);
494 return 0 unless $reserve->borrowernumber == $borrowernumber;
495 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
497 return 1;
501 =head2 GetOtherReserves
503 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
505 Check queued list of this document and check if this document must be transferred
507 =cut
509 sub GetOtherReserves {
510 my ($itemnumber) = @_;
511 my $messages;
512 my $nextreservinfo;
513 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
514 if ($checkreserves) {
515 my $iteminfo = GetItem($itemnumber);
516 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
517 $messages->{'transfert'} = $checkreserves->{'branchcode'};
518 #minus priorities of others reservs
519 ModReserveMinusPriority(
520 $itemnumber,
521 $checkreserves->{'reserve_id'},
524 #launch the subroutine dotransfer
525 C4::Items::ModItemTransfer(
526 $itemnumber,
527 $iteminfo->{'holdingbranch'},
528 $checkreserves->{'branchcode'}
533 #step 2b : case of a reservation on the same branch, set the waiting status
534 else {
535 $messages->{'waiting'} = 1;
536 ModReserveMinusPriority(
537 $itemnumber,
538 $checkreserves->{'reserve_id'},
540 ModReserveStatus($itemnumber,'W');
543 $nextreservinfo = $checkreserves->{'borrowernumber'};
546 return ( $messages, $nextreservinfo );
549 =head2 ChargeReserveFee
551 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
553 Charge the fee for a reserve (if $fee > 0)
555 =cut
557 sub ChargeReserveFee {
558 my ( $borrowernumber, $fee, $title ) = @_;
559 return if !$fee || $fee==0; # the last test is needed to include 0.00
560 my $accquery = qq{
561 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
563 my $dbh = C4::Context->dbh;
564 my $nextacctno = &getnextacctno( $borrowernumber );
565 $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
568 =head2 GetReserveFee
570 $fee = GetReserveFee( $borrowernumber, $biblionumber );
572 Calculate the fee for a reserve (if applicable).
574 =cut
576 sub GetReserveFee {
577 my ( $borrowernumber, $biblionumber ) = @_;
578 my $borquery = qq{
579 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
581 my $issue_qry = qq{
582 SELECT COUNT(*) FROM items
583 LEFT JOIN issues USING (itemnumber)
584 WHERE items.biblionumber=? AND issues.issue_id IS NULL
586 my $holds_qry = qq{
587 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
590 my $dbh = C4::Context->dbh;
591 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
592 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
593 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
594 # This is a reconstruction of the old code:
595 # Compare number of items with items issued, and optionally check holds
596 # If not all items are issued and there are no holds: charge no fee
597 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
598 my ( $notissued, $reserved );
599 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
600 ( $biblionumber ) );
601 if( $notissued ) {
602 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
603 ( $biblionumber, $borrowernumber ) );
604 $fee = 0 if $reserved == 0;
607 return $fee;
610 =head2 GetReserveStatus
612 $reservestatus = GetReserveStatus($itemnumber);
614 Takes an itemnumber and returns the status of the reserve placed on it.
615 If several reserves exist, the reserve with the lower priority is given.
617 =cut
619 ## FIXME: I don't think this does what it thinks it does.
620 ## It only ever checks the first reserve result, even though
621 ## multiple reserves for that bib can have the itemnumber set
622 ## the sub is only used once in the codebase.
623 sub GetReserveStatus {
624 my ($itemnumber) = @_;
626 my $dbh = C4::Context->dbh;
628 my ($sth, $found, $priority);
629 if ( $itemnumber ) {
630 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
631 $sth->execute($itemnumber);
632 ($found, $priority) = $sth->fetchrow_array;
635 if(defined $found) {
636 return 'Waiting' if $found eq 'W' and $priority == 0;
637 return 'Finished' if $found eq 'F';
640 return 'Reserved' if $priority > 0;
642 return ''; # empty string here will remove need for checking undef, or less log lines
645 =head2 CheckReserves
647 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
648 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
649 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
651 Find a book in the reserves.
653 C<$itemnumber> is the book's item number.
654 C<$lookahead> is the number of days to look in advance for future reserves.
656 As I understand it, C<&CheckReserves> looks for the given item in the
657 reserves. If it is found, that's a match, and C<$status> is set to
658 C<Waiting>.
660 Otherwise, it finds the most important item in the reserves with the
661 same biblio number as this book (I'm not clear on this) and returns it
662 with C<$status> set to C<Reserved>.
664 C<&CheckReserves> returns a two-element list:
666 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
668 C<$reserve> is the reserve item that matched. It is a
669 reference-to-hash whose keys are mostly the fields of the reserves
670 table in the Koha database.
672 =cut
674 sub CheckReserves {
675 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
676 my $dbh = C4::Context->dbh;
677 my $sth;
678 my $select;
679 if (C4::Context->preference('item-level_itypes')){
680 $select = "
681 SELECT items.biblionumber,
682 items.biblioitemnumber,
683 itemtypes.notforloan,
684 items.notforloan AS itemnotforloan,
685 items.itemnumber,
686 items.damaged,
687 items.homebranch,
688 items.holdingbranch
689 FROM items
690 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
691 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
694 else {
695 $select = "
696 SELECT items.biblionumber,
697 items.biblioitemnumber,
698 itemtypes.notforloan,
699 items.notforloan AS itemnotforloan,
700 items.itemnumber,
701 items.damaged,
702 items.homebranch,
703 items.holdingbranch
704 FROM items
705 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
706 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
710 if ($item) {
711 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
712 $sth->execute($item);
714 else {
715 $sth = $dbh->prepare("$select WHERE barcode = ?");
716 $sth->execute($barcode);
718 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
719 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
721 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
723 return unless $itemnumber; # bail if we got nothing.
725 # if item is not for loan it cannot be reserved either.....
726 # except where items.notforloan < 0 : This indicates the item is holdable.
727 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
729 # Find this item in the reserves
730 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
732 # $priority and $highest are used to find the most important item
733 # in the list returned by &_Findgroupreserve. (The lower $priority,
734 # the more important the item.)
735 # $highest is the most important item we've seen so far.
736 my $highest;
737 if (scalar @reserves) {
738 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
739 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
740 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
742 my $priority = 10000000;
743 foreach my $res (@reserves) {
744 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
745 if ($res->{'found'} eq 'W') {
746 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
747 } else {
748 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
750 } else {
751 my $patron;
752 my $iteminfo;
753 my $local_hold_match;
755 if ($LocalHoldsPriority) {
756 $patron = Koha::Patrons->find( $res->{borrowernumber} );
757 $iteminfo = C4::Items::GetItem($itemnumber);
759 my $local_holds_priority_item_branchcode =
760 $iteminfo->{$LocalHoldsPriorityItemControl};
761 my $local_holds_priority_patron_branchcode =
762 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
763 ? $res->{branchcode}
764 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
765 ? $patron->branchcode
766 : undef;
767 $local_hold_match =
768 $local_holds_priority_item_branchcode eq
769 $local_holds_priority_patron_branchcode;
772 # See if this item is more important than what we've got so far
773 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
774 $iteminfo ||= C4::Items::GetItem($itemnumber);
775 next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
776 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
777 my $branch = GetReservesControlBranch( $iteminfo, $patron->unblessed );
778 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
779 next if ($branchitemrule->{'holdallowed'} == 0);
780 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
781 next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
782 $priority = $res->{'priority'};
783 $highest = $res;
784 last if $local_hold_match;
790 # If we get this far, then no exact match was found.
791 # We return the most important (i.e. next) reservation.
792 if ($highest) {
793 $highest->{'itemnumber'} = $item;
794 return ( "Reserved", $highest, \@reserves );
797 return ( '' );
800 =head2 CancelExpiredReserves
802 CancelExpiredReserves();
804 Cancels all reserves with an expiration date from before today.
806 =cut
808 sub CancelExpiredReserves {
809 my $today = dt_from_string();
810 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
811 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
813 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
814 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
815 $params->{found} = undef unless $expireWaiting;
817 # FIXME To move to Koha::Holds->search_expired (?)
818 my $holds = Koha::Holds->search( $params );
820 while ( my $hold = $holds->next ) {
821 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
823 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
825 my $cancel_params = {};
826 if ( $hold->found eq 'W' ) {
827 $cancel_params->{charge_cancel_fee} = 1;
829 $hold->cancel( $cancel_params );
833 =head2 AutoUnsuspendReserves
835 AutoUnsuspendReserves();
837 Unsuspends all suspended reserves with a suspend_until date from before today.
839 =cut
841 sub AutoUnsuspendReserves {
842 my $today = dt_from_string();
844 my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
846 map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
849 =head2 ModReserve
851 ModReserve({ rank => $rank,
852 reserve_id => $reserve_id,
853 branchcode => $branchcode
854 [, itemnumber => $itemnumber ]
855 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
858 Change a hold request's priority or cancel it.
860 C<$rank> specifies the effect of the change. If C<$rank>
861 is 'W' or 'n', nothing happens. This corresponds to leaving a
862 request alone when changing its priority in the holds queue
863 for a bib.
865 If C<$rank> is 'del', the hold request is cancelled.
867 If C<$rank> is an integer greater than zero, the priority of
868 the request is set to that value. Since priority != 0 means
869 that the item is not waiting on the hold shelf, setting the
870 priority to a non-zero value also sets the request's found
871 status and waiting date to NULL.
873 The optional C<$itemnumber> parameter is used only when
874 C<$rank> is a non-zero integer; if supplied, the itemnumber
875 of the hold request is set accordingly; if omitted, the itemnumber
876 is cleared.
878 B<FIXME:> Note that the forgoing can have the effect of causing
879 item-level hold requests to turn into title-level requests. This
880 will be fixed once reserves has separate columns for requested
881 itemnumber and supplying itemnumber.
883 =cut
885 sub ModReserve {
886 my ( $params ) = @_;
888 my $rank = $params->{'rank'};
889 my $reserve_id = $params->{'reserve_id'};
890 my $branchcode = $params->{'branchcode'};
891 my $itemnumber = $params->{'itemnumber'};
892 my $suspend_until = $params->{'suspend_until'};
893 my $borrowernumber = $params->{'borrowernumber'};
894 my $biblionumber = $params->{'biblionumber'};
896 return if $rank eq "W";
897 return if $rank eq "n";
899 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
901 my $hold;
902 unless ( $reserve_id ) {
903 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
904 return unless $holds->count; # FIXME Should raise an exception
905 $hold = $holds->next;
906 $reserve_id = $hold->reserve_id;
909 $hold ||= Koha::Holds->find($reserve_id);
911 if ( $rank eq "del" ) {
912 $hold->cancel;
914 elsif ($rank =~ /^\d+/ and $rank > 0) {
915 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
916 if C4::Context->preference('HoldsLog');
918 $hold->set(
920 priority => $rank,
921 branchcode => $branchcode,
922 itemnumber => $itemnumber,
923 found => undef,
924 waitingdate => undef
926 )->store();
928 if ( defined( $suspend_until ) ) {
929 if ( $suspend_until ) {
930 $suspend_until = eval { dt_from_string( $suspend_until ) };
931 $hold->suspend_hold( $suspend_until );
932 } else {
933 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
934 # If the hold is not suspended, this does nothing.
935 $hold->set( { suspend_until => undef } )->store();
939 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
943 =head2 ModReserveFill
945 &ModReserveFill($reserve);
947 Fill a reserve. If I understand this correctly, this means that the
948 reserved book has been found and given to the patron who reserved it.
950 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
951 whose keys are fields from the reserves table in the Koha database.
953 =cut
955 sub ModReserveFill {
956 my ($res) = @_;
957 my $reserve_id = $res->{'reserve_id'};
959 my $hold = Koha::Holds->find($reserve_id);
961 # get the priority on this record....
962 my $priority = $hold->priority;
964 # update the hold statuses, no need to store it though, we will be deleting it anyway
965 $hold->set(
967 found => 'F',
968 priority => 0,
972 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
973 Koha::Old::Hold->new( $hold->unblessed() )->store();
975 $hold->delete();
977 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
978 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
979 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
982 # now fix the priority on the others (if the priority wasn't
983 # already sorted!)....
984 unless ( $priority == 0 ) {
985 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
989 =head2 ModReserveStatus
991 &ModReserveStatus($itemnumber, $newstatus);
993 Update the reserve status for the active (priority=0) reserve.
995 $itemnumber is the itemnumber the reserve is on
997 $newstatus is the new status.
999 =cut
1001 sub ModReserveStatus {
1003 #first : check if we have a reservation for this item .
1004 my ($itemnumber, $newstatus) = @_;
1005 my $dbh = C4::Context->dbh;
1007 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1008 my $sth_set = $dbh->prepare($query);
1009 $sth_set->execute( $newstatus, $itemnumber );
1011 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1012 CartToShelf( $itemnumber );
1016 =head2 ModReserveAffect
1018 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1020 This function affect an item and a status for a given reserve, either fetched directly
1021 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1022 is given, only first reserve returned is affected, which is ok for anything but
1023 multi-item holds.
1025 if $transferToDo is not set, then the status is set to "Waiting" as well.
1026 otherwise, a transfer is on the way, and the end of the transfer will
1027 take care of the waiting status
1029 =cut
1031 sub ModReserveAffect {
1032 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1033 my $dbh = C4::Context->dbh;
1035 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1036 # attached to $itemnumber
1037 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1038 $sth->execute($itemnumber);
1039 my ($biblionumber) = $sth->fetchrow;
1041 # get request - need to find out if item is already
1042 # waiting in order to not send duplicate hold filled notifications
1044 my $hold;
1045 # Find hold by id if we have it
1046 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1047 # Find item level hold for this item if there is one
1048 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1049 # Find record level hold if there is no item level hold
1050 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1052 return unless $hold;
1054 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1056 $hold->itemnumber($itemnumber);
1057 $hold->set_waiting($transferToDo);
1059 _koha_notify_reserve( $hold->reserve_id )
1060 if ( !$transferToDo && !$already_on_shelf );
1062 _FixPriority( { biblionumber => $biblionumber } );
1064 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1065 CartToShelf($itemnumber);
1068 return;
1071 =head2 ModReserveCancelAll
1073 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1075 function to cancel reserv,check other reserves, and transfer document if it's necessary
1077 =cut
1079 sub ModReserveCancelAll {
1080 my $messages;
1081 my $nextreservinfo;
1082 my ( $itemnumber, $borrowernumber ) = @_;
1084 #step 1 : cancel the reservation
1085 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1086 return unless $holds->count;
1087 $holds->next->cancel;
1089 #step 2 launch the subroutine of the others reserves
1090 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1092 return ( $messages, $nextreservinfo );
1095 =head2 ModReserveMinusPriority
1097 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1099 Reduce the values of queued list
1101 =cut
1103 sub ModReserveMinusPriority {
1104 my ( $itemnumber, $reserve_id ) = @_;
1106 #first step update the value of the first person on reserv
1107 my $dbh = C4::Context->dbh;
1108 my $query = "
1109 UPDATE reserves
1110 SET priority = 0 , itemnumber = ?
1111 WHERE reserve_id = ?
1113 my $sth_upd = $dbh->prepare($query);
1114 $sth_upd->execute( $itemnumber, $reserve_id );
1115 # second step update all others reserves
1116 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1119 =head2 IsAvailableForItemLevelRequest
1121 my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1123 Checks whether a given item record is available for an
1124 item-level hold request. An item is available if
1126 * it is not lost AND
1127 * it is not damaged AND
1128 * it is not withdrawn AND
1129 * a waiting or in transit reserve is placed on
1130 * does not have a not for loan value > 0
1132 Need to check the issuingrules onshelfholds column,
1133 if this is set items on the shelf can be placed on hold
1135 Note that IsAvailableForItemLevelRequest() does not
1136 check if the staff operator is authorized to place
1137 a request on the item - in particular,
1138 this routine does not check IndependentBranches
1139 and canreservefromotherbranches.
1141 =cut
1143 sub IsAvailableForItemLevelRequest {
1144 my $item = shift;
1145 my $borrower = shift;
1147 my $dbh = C4::Context->dbh;
1148 # must check the notforloan setting of the itemtype
1149 # FIXME - a lot of places in the code do this
1150 # or something similar - need to be
1151 # consolidated
1152 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
1153 my $item_object = Koha::Items->find( $item->{itemnumber } );
1154 my $itemtype = $item_object->effective_itemtype;
1155 my $notforloan_per_itemtype
1156 = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1157 undef, $itemtype);
1159 return 0 if
1160 $notforloan_per_itemtype ||
1161 $item->{itemlost} ||
1162 $item->{notforloan} > 0 ||
1163 $item->{withdrawn} ||
1164 ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1166 my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item_object, patron => $patron } );
1168 if ( $on_shelf_holds == 1 ) {
1169 return 1;
1170 } elsif ( $on_shelf_holds == 2 ) {
1171 my @items =
1172 Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1174 my $any_available = 0;
1176 foreach my $i (@items) {
1178 my $circ_control_branch = C4::Circulation::_GetCircControlBranch( $i->unblessed(), $borrower );
1179 my $branchitemrule = C4::Circulation::GetBranchItemRule( $circ_control_branch, $i->itype );
1181 $any_available = 1
1182 unless $i->itemlost
1183 || $i->notforloan > 0
1184 || $i->withdrawn
1185 || $i->onloan
1186 || IsItemOnHoldAndFound( $i->id )
1187 || ( $i->damaged
1188 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1189 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1190 || $branchitemrule->{holdallowed} == 1 && $borrower->{branchcode} ne $i->homebranch;
1193 return $any_available ? 0 : 1;
1194 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1195 return $item->{onloan} || IsItemOnHoldAndFound( $item->{itemnumber} );
1199 sub _get_itype {
1200 my $item = shift;
1202 my $itype;
1203 if (C4::Context->preference('item-level_itypes')) {
1204 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1205 # When GetItem is fixed, we can remove this
1206 $itype = $item->{itype};
1208 else {
1209 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1210 # So if we already have a biblioitems join when calling this function,
1211 # we don't need to access the database again
1212 $itype = $item->{itemtype};
1214 unless ($itype) {
1215 my $dbh = C4::Context->dbh;
1216 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1217 my $sth = $dbh->prepare($query);
1218 $sth->execute($item->{biblioitemnumber});
1219 if (my $data = $sth->fetchrow_hashref()){
1220 $itype = $data->{itemtype};
1223 return $itype;
1226 =head2 AlterPriority
1228 AlterPriority( $where, $reserve_id );
1230 This function changes a reserve's priority up, down, to the top, or to the bottom.
1231 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1233 =cut
1235 sub AlterPriority {
1236 my ( $where, $reserve_id ) = @_;
1238 my $hold = Koha::Holds->find( $reserve_id );
1239 return unless $hold;
1241 if ( $hold->cancellationdate ) {
1242 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1243 return;
1246 if ( $where eq 'up' || $where eq 'down' ) {
1248 my $priority = $hold->priority;
1249 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1250 _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1252 } elsif ( $where eq 'top' ) {
1254 _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1256 } elsif ( $where eq 'bottom' ) {
1258 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1261 # FIXME Should return the new priority
1264 =head2 ToggleLowestPriority
1266 ToggleLowestPriority( $borrowernumber, $biblionumber );
1268 This function sets the lowestPriority field to true if is false, and false if it is true.
1270 =cut
1272 sub ToggleLowestPriority {
1273 my ( $reserve_id ) = @_;
1275 my $dbh = C4::Context->dbh;
1277 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1278 $sth->execute( $reserve_id );
1280 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1283 =head2 ToggleSuspend
1285 ToggleSuspend( $reserve_id );
1287 This function sets the suspend field to true if is false, and false if it is true.
1288 If the reserve is currently suspended with a suspend_until date, that date will
1289 be cleared when it is unsuspended.
1291 =cut
1293 sub ToggleSuspend {
1294 my ( $reserve_id, $suspend_until ) = @_;
1296 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1298 my $hold = Koha::Holds->find( $reserve_id );
1300 if ( $hold->is_suspended ) {
1301 $hold->resume()
1302 } else {
1303 $hold->suspend_hold( $suspend_until );
1307 =head2 SuspendAll
1309 SuspendAll(
1310 borrowernumber => $borrowernumber,
1311 [ biblionumber => $biblionumber, ]
1312 [ suspend_until => $suspend_until, ]
1313 [ suspend => $suspend ]
1316 This function accepts a set of hash keys as its parameters.
1317 It requires either borrowernumber or biblionumber, or both.
1319 suspend_until is wholly optional.
1321 =cut
1323 sub SuspendAll {
1324 my %params = @_;
1326 my $borrowernumber = $params{'borrowernumber'} || undef;
1327 my $biblionumber = $params{'biblionumber'} || undef;
1328 my $suspend_until = $params{'suspend_until'} || undef;
1329 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1331 $suspend_until = eval { dt_from_string($suspend_until) }
1332 if ( defined($suspend_until) );
1334 return unless ( $borrowernumber || $biblionumber );
1336 my $params;
1337 $params->{found} = undef;
1338 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1339 $params->{biblionumber} = $biblionumber if $biblionumber;
1341 my @holds = Koha::Holds->search($params);
1343 if ($suspend) {
1344 map { $_->suspend_hold($suspend_until) } @holds;
1346 else {
1347 map { $_->resume() } @holds;
1352 =head2 _FixPriority
1354 _FixPriority({
1355 reserve_id => $reserve_id,
1356 [rank => $rank,]
1357 [ignoreSetLowestRank => $ignoreSetLowestRank]
1362 _FixPriority({ biblionumber => $biblionumber});
1364 This routine adjusts the priority of a hold request and holds
1365 on the same bib.
1367 In the first form, where a reserve_id is passed, the priority of the
1368 hold is set to supplied rank, and other holds for that bib are adjusted
1369 accordingly. If the rank is "del", the hold is cancelled. If no rank
1370 is supplied, all of the holds on that bib have their priority adjusted
1371 as if the second form had been used.
1373 In the second form, where a biblionumber is passed, the holds on that
1374 bib (that are not captured) are sorted in order of increasing priority,
1375 then have reserves.priority set so that the first non-captured hold
1376 has its priority set to 1, the second non-captured hold has its priority
1377 set to 2, and so forth.
1379 In both cases, holds that have the lowestPriority flag on are have their
1380 priority adjusted to ensure that they remain at the end of the line.
1382 Note that the ignoreSetLowestRank parameter is meant to be used only
1383 when _FixPriority calls itself.
1385 =cut
1387 sub _FixPriority {
1388 my ( $params ) = @_;
1389 my $reserve_id = $params->{reserve_id};
1390 my $rank = $params->{rank} // '';
1391 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1392 my $biblionumber = $params->{biblionumber};
1394 my $dbh = C4::Context->dbh;
1396 my $hold;
1397 if ( $reserve_id ) {
1398 $hold = Koha::Holds->find( $reserve_id );
1399 return unless $hold;
1402 unless ( $biblionumber ) { # FIXME This is a very weird API
1403 $biblionumber = $hold->biblionumber;
1406 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1407 $hold->cancel;
1409 elsif ( $rank eq "W" || $rank eq "0" ) {
1411 # make sure priority for waiting or in-transit items is 0
1412 my $query = "
1413 UPDATE reserves
1414 SET priority = 0
1415 WHERE reserve_id = ?
1416 AND found IN ('W', 'T')
1418 my $sth = $dbh->prepare($query);
1419 $sth->execute( $reserve_id );
1421 my @priority;
1423 # get whats left
1424 my $query = "
1425 SELECT reserve_id, borrowernumber, reservedate
1426 FROM reserves
1427 WHERE biblionumber = ?
1428 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1429 ORDER BY priority ASC
1431 my $sth = $dbh->prepare($query);
1432 $sth->execute( $biblionumber );
1433 while ( my $line = $sth->fetchrow_hashref ) {
1434 push( @priority, $line );
1437 # To find the matching index
1438 my $i;
1439 my $key = -1; # to allow for 0 to be a valid result
1440 for ( $i = 0 ; $i < @priority ; $i++ ) {
1441 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1442 $key = $i; # save the index
1443 last;
1447 # if index exists in array then move it to new position
1448 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1449 my $new_rank = $rank -
1450 1; # $new_rank is what you want the new index to be in the array
1451 my $moving_item = splice( @priority, $key, 1 );
1452 splice( @priority, $new_rank, 0, $moving_item );
1455 # now fix the priority on those that are left....
1456 $query = "
1457 UPDATE reserves
1458 SET priority = ?
1459 WHERE reserve_id = ?
1461 $sth = $dbh->prepare($query);
1462 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1463 $sth->execute(
1464 $j + 1,
1465 $priority[$j]->{'reserve_id'}
1469 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1470 $sth->execute();
1472 unless ( $ignoreSetLowestRank ) {
1473 while ( my $res = $sth->fetchrow_hashref() ) {
1474 _FixPriority({
1475 reserve_id => $res->{'reserve_id'},
1476 rank => '999999',
1477 ignoreSetLowestRank => 1
1483 =head2 _Findgroupreserve
1485 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1487 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1488 first match found. If neither, then we look for non-holds-queue based holds.
1489 Lookahead is the number of days to look in advance.
1491 C<&_Findgroupreserve> returns :
1492 C<@results> is an array of references-to-hash whose keys are mostly
1493 fields from the reserves table of the Koha database, plus
1494 C<biblioitemnumber>.
1496 =cut
1498 sub _Findgroupreserve {
1499 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1500 my $dbh = C4::Context->dbh;
1502 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1503 # check for exact targeted match
1504 my $item_level_target_query = qq{
1505 SELECT reserves.biblionumber AS biblionumber,
1506 reserves.borrowernumber AS borrowernumber,
1507 reserves.reservedate AS reservedate,
1508 reserves.branchcode AS branchcode,
1509 reserves.cancellationdate AS cancellationdate,
1510 reserves.found AS found,
1511 reserves.reservenotes AS reservenotes,
1512 reserves.priority AS priority,
1513 reserves.timestamp AS timestamp,
1514 biblioitems.biblioitemnumber AS biblioitemnumber,
1515 reserves.itemnumber AS itemnumber,
1516 reserves.reserve_id AS reserve_id,
1517 reserves.itemtype AS itemtype
1518 FROM reserves
1519 JOIN biblioitems USING (biblionumber)
1520 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1521 WHERE found IS NULL
1522 AND priority > 0
1523 AND item_level_request = 1
1524 AND itemnumber = ?
1525 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1526 AND suspend = 0
1527 ORDER BY priority
1529 my $sth = $dbh->prepare($item_level_target_query);
1530 $sth->execute($itemnumber, $lookahead||0);
1531 my @results;
1532 if ( my $data = $sth->fetchrow_hashref ) {
1533 push( @results, $data )
1534 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1536 return @results if @results;
1538 # check for title-level targeted match
1539 my $title_level_target_query = qq{
1540 SELECT reserves.biblionumber AS biblionumber,
1541 reserves.borrowernumber AS borrowernumber,
1542 reserves.reservedate AS reservedate,
1543 reserves.branchcode AS branchcode,
1544 reserves.cancellationdate AS cancellationdate,
1545 reserves.found AS found,
1546 reserves.reservenotes AS reservenotes,
1547 reserves.priority AS priority,
1548 reserves.timestamp AS timestamp,
1549 biblioitems.biblioitemnumber AS biblioitemnumber,
1550 reserves.itemnumber AS itemnumber,
1551 reserves.reserve_id AS reserve_id,
1552 reserves.itemtype AS itemtype
1553 FROM reserves
1554 JOIN biblioitems USING (biblionumber)
1555 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1556 WHERE found IS NULL
1557 AND priority > 0
1558 AND item_level_request = 0
1559 AND hold_fill_targets.itemnumber = ?
1560 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1561 AND suspend = 0
1562 ORDER BY priority
1564 $sth = $dbh->prepare($title_level_target_query);
1565 $sth->execute($itemnumber, $lookahead||0);
1566 @results = ();
1567 if ( my $data = $sth->fetchrow_hashref ) {
1568 push( @results, $data )
1569 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1571 return @results if @results;
1573 my $query = qq{
1574 SELECT reserves.biblionumber AS biblionumber,
1575 reserves.borrowernumber AS borrowernumber,
1576 reserves.reservedate AS reservedate,
1577 reserves.waitingdate AS waitingdate,
1578 reserves.branchcode AS branchcode,
1579 reserves.cancellationdate AS cancellationdate,
1580 reserves.found AS found,
1581 reserves.reservenotes AS reservenotes,
1582 reserves.priority AS priority,
1583 reserves.timestamp AS timestamp,
1584 reserves.itemnumber AS itemnumber,
1585 reserves.reserve_id AS reserve_id,
1586 reserves.itemtype AS itemtype
1587 FROM reserves
1588 WHERE reserves.biblionumber = ?
1589 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1590 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1591 AND suspend = 0
1592 ORDER BY priority
1594 $sth = $dbh->prepare($query);
1595 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1596 @results = ();
1597 while ( my $data = $sth->fetchrow_hashref ) {
1598 push( @results, $data )
1599 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1601 return @results;
1604 =head2 _koha_notify_reserve
1606 _koha_notify_reserve( $hold->reserve_id );
1608 Sends a notification to the patron that their hold has been filled (through
1609 ModReserveAffect, _not_ ModReserveFill)
1611 The letter code for this notice may be found using the following query:
1613 select distinct letter_code
1614 from message_transports
1615 inner join message_attributes using (message_attribute_id)
1616 where message_name = 'Hold_Filled'
1618 This will probably sipmly be 'HOLD', but because it is defined in the database,
1619 it is subject to addition or change.
1621 The following tables are availalbe witin the notice:
1623 branches
1624 borrowers
1625 biblio
1626 biblioitems
1627 reserves
1628 items
1630 =cut
1632 sub _koha_notify_reserve {
1633 my $reserve_id = shift;
1634 my $hold = Koha::Holds->find($reserve_id);
1635 my $borrowernumber = $hold->borrowernumber;
1637 my $patron = Koha::Patrons->find( $borrowernumber );
1639 # Try to get the borrower's email address
1640 my $to_address = $patron->notice_email_address;
1642 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1643 borrowernumber => $borrowernumber,
1644 message_name => 'Hold_Filled'
1645 } );
1647 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1649 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1651 my %letter_params = (
1652 module => 'reserves',
1653 branchcode => $hold->branchcode,
1654 lang => $patron->lang,
1655 tables => {
1656 'branches' => $library,
1657 'borrowers' => $patron->unblessed,
1658 'biblio' => $hold->biblionumber,
1659 'biblioitems' => $hold->biblionumber,
1660 'reserves' => $hold->unblessed,
1661 'items' => $hold->itemnumber,
1665 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.
1666 my $send_notification = sub {
1667 my ( $mtt, $letter_code ) = (@_);
1668 return unless defined $letter_code;
1669 $letter_params{letter_code} = $letter_code;
1670 $letter_params{message_transport_type} = $mtt;
1671 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1672 unless ($letter) {
1673 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1674 return;
1677 C4::Letters::EnqueueLetter( {
1678 letter => $letter,
1679 borrowernumber => $borrowernumber,
1680 from_address => $admin_email_address,
1681 message_transport_type => $mtt,
1682 } );
1685 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1686 next if (
1687 ( $mtt eq 'email' and not $to_address ) # No email address
1688 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1689 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1692 &$send_notification($mtt, $letter_code);
1693 $notification_sent++;
1695 #Making sure that a print notification is sent if no other transport types can be utilized.
1696 if (! $notification_sent) {
1697 &$send_notification('print', 'HOLD');
1702 =head2 _ShiftPriorityByDateAndPriority
1704 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1706 This increments the priority of all reserves after the one
1707 with either the lowest date after C<$reservedate>
1708 or the lowest priority after C<$priority>.
1710 It effectively makes room for a new reserve to be inserted with a certain
1711 priority, which is returned.
1713 This is most useful when the reservedate can be set by the user. It allows
1714 the new reserve to be placed before other reserves that have a later
1715 reservedate. Since priority also is set by the form in reserves/request.pl
1716 the sub accounts for that too.
1718 =cut
1720 sub _ShiftPriorityByDateAndPriority {
1721 my ( $biblio, $resdate, $new_priority ) = @_;
1723 my $dbh = C4::Context->dbh;
1724 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1725 my $sth = $dbh->prepare( $query );
1726 $sth->execute( $biblio, $resdate, $new_priority );
1727 my $min_priority = $sth->fetchrow;
1728 # if no such matches are found, $new_priority remains as original value
1729 $new_priority = $min_priority if ( $min_priority );
1731 # Shift the priority up by one; works in conjunction with the next SQL statement
1732 $query = "UPDATE reserves
1733 SET priority = priority+1
1734 WHERE biblionumber = ?
1735 AND borrowernumber = ?
1736 AND reservedate = ?
1737 AND found IS NULL";
1738 my $sth_update = $dbh->prepare( $query );
1740 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1741 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1742 $sth = $dbh->prepare( $query );
1743 $sth->execute( $new_priority, $biblio );
1744 while ( my $row = $sth->fetchrow_hashref ) {
1745 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1748 return $new_priority; # so the caller knows what priority they wind up receiving
1751 =head2 MoveReserve
1753 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1755 Use when checking out an item to handle reserves
1756 If $cancelreserve boolean is set to true, it will remove existing reserve
1758 =cut
1760 sub MoveReserve {
1761 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1763 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1764 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
1765 return unless $res;
1767 my $biblionumber = $res->{biblionumber};
1769 if ($res->{borrowernumber} == $borrowernumber) {
1770 ModReserveFill($res);
1772 else {
1773 # warn "Reserved";
1774 # The item is reserved by someone else.
1775 # Find this item in the reserves
1777 my $borr_res;
1778 foreach (@$all_reserves) {
1779 $_->{'borrowernumber'} == $borrowernumber or next;
1780 $_->{'biblionumber'} == $biblionumber or next;
1782 $borr_res = $_;
1783 last;
1786 if ( $borr_res ) {
1787 # The item is reserved by the current patron
1788 ModReserveFill($borr_res);
1791 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1792 RevertWaitingStatus({ itemnumber => $itemnumber });
1794 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1795 my $hold = Koha::Holds->find( $res->{reserve_id} );
1796 $hold->cancel;
1801 =head2 MergeHolds
1803 MergeHolds($dbh,$to_biblio, $from_biblio);
1805 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1807 =cut
1809 sub MergeHolds {
1810 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1811 my $sth = $dbh->prepare(
1812 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1814 $sth->execute($from_biblio);
1815 if ( my $data = $sth->fetchrow_hashref() ) {
1817 # holds exist on old record, if not we don't need to do anything
1818 $sth = $dbh->prepare(
1819 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1820 $sth->execute( $to_biblio, $from_biblio );
1822 # Reorder by date
1823 # don't reorder those already waiting
1825 $sth = $dbh->prepare(
1826 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1828 my $upd_sth = $dbh->prepare(
1829 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1830 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1832 $sth->execute( $to_biblio, 'W', 'T' );
1833 my $priority = 1;
1834 while ( my $reserve = $sth->fetchrow_hashref() ) {
1835 $upd_sth->execute(
1836 $priority, $to_biblio,
1837 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1838 $reserve->{'itemnumber'}
1840 $priority++;
1845 =head2 RevertWaitingStatus
1847 RevertWaitingStatus({ itemnumber => $itemnumber });
1849 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1851 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1852 item level hold, even if it was only a bibliolevel hold to
1853 begin with. This is because we can no longer know if a hold
1854 was item-level or bib-level after a hold has been set to
1855 waiting status.
1857 =cut
1859 sub RevertWaitingStatus {
1860 my ( $params ) = @_;
1861 my $itemnumber = $params->{'itemnumber'};
1863 return unless ( $itemnumber );
1865 my $dbh = C4::Context->dbh;
1867 ## Get the waiting reserve we want to revert
1868 my $query = "
1869 SELECT * FROM reserves
1870 WHERE itemnumber = ?
1871 AND found IS NOT NULL
1873 my $sth = $dbh->prepare( $query );
1874 $sth->execute( $itemnumber );
1875 my $reserve = $sth->fetchrow_hashref();
1877 ## Increment the priority of all other non-waiting
1878 ## reserves for this bib record
1879 $query = "
1880 UPDATE reserves
1882 priority = priority + 1
1883 WHERE
1884 biblionumber = ?
1886 priority > 0
1888 $sth = $dbh->prepare( $query );
1889 $sth->execute( $reserve->{'biblionumber'} );
1891 ## Fix up the currently waiting reserve
1892 $query = "
1893 UPDATE reserves
1895 priority = 1,
1896 found = NULL,
1897 waitingdate = NULL
1898 WHERE
1899 reserve_id = ?
1901 $sth = $dbh->prepare( $query );
1902 $sth->execute( $reserve->{'reserve_id'} );
1903 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1906 =head2 ReserveSlip
1908 ReserveSlip(
1910 branchcode => $branchcode,
1911 borrowernumber => $borrowernumber,
1912 biblionumber => $biblionumber,
1913 [ itemnumber => $itemnumber, ]
1914 [ barcode => $barcode, ]
1918 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1920 The letter code will be HOLD_SLIP, and the following tables are
1921 available within the slip:
1923 reserves
1924 branches
1925 borrowers
1926 biblio
1927 biblioitems
1928 items
1930 =cut
1932 sub ReserveSlip {
1933 my ($args) = @_;
1934 my $branchcode = $args->{branchcode};
1935 my $borrowernumber = $args->{borrowernumber};
1936 my $biblionumber = $args->{biblionumber};
1937 my $itemnumber = $args->{itemnumber};
1938 my $barcode = $args->{barcode};
1941 my $patron = Koha::Patrons->find($borrowernumber);
1943 my $hold;
1944 if ($itemnumber || $barcode ) {
1945 $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
1947 $hold = Koha::Holds->search(
1949 biblionumber => $biblionumber,
1950 borrowernumber => $borrowernumber,
1951 itemnumber => $itemnumber
1953 )->next;
1955 else {
1956 $hold = Koha::Holds->search(
1958 biblionumber => $biblionumber,
1959 borrowernumber => $borrowernumber
1961 )->next;
1964 return unless $hold;
1965 my $reserve = $hold->unblessed;
1967 return C4::Letters::GetPreparedLetter (
1968 module => 'circulation',
1969 letter_code => 'HOLD_SLIP',
1970 branchcode => $branchcode,
1971 lang => $patron->lang,
1972 tables => {
1973 'reserves' => $reserve,
1974 'branches' => $reserve->{branchcode},
1975 'borrowers' => $reserve->{borrowernumber},
1976 'biblio' => $reserve->{biblionumber},
1977 'biblioitems' => $reserve->{biblionumber},
1978 'items' => $reserve->{itemnumber},
1983 =head2 GetReservesControlBranch
1985 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
1987 Return the branchcode to be used to determine which reserves
1988 policy applies to a transaction.
1990 C<$item> is a hashref for an item. Only 'homebranch' is used.
1992 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
1994 =cut
1996 sub GetReservesControlBranch {
1997 my ( $item, $borrower ) = @_;
1999 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2001 my $branchcode =
2002 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2003 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2004 : undef;
2006 return $branchcode;
2009 =head2 CalculatePriority
2011 my $p = CalculatePriority($biblionumber, $resdate);
2013 Calculate priority for a new reserve on biblionumber, placing it at
2014 the end of the line of all holds whose start date falls before
2015 the current system time and that are neither on the hold shelf
2016 or in transit.
2018 The reserve date parameter is optional; if it is supplied, the
2019 priority is based on the set of holds whose start date falls before
2020 the parameter value.
2022 After calculation of this priority, it is recommended to call
2023 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2024 AddReserves.
2026 =cut
2028 sub CalculatePriority {
2029 my ( $biblionumber, $resdate ) = @_;
2031 my $sql = q{
2032 SELECT COUNT(*) FROM reserves
2033 WHERE biblionumber = ?
2034 AND priority > 0
2035 AND (found IS NULL OR found = '')
2037 #skip found==W or found==T (waiting or transit holds)
2038 if( $resdate ) {
2039 $sql.= ' AND ( reservedate <= ? )';
2041 else {
2042 $sql.= ' AND ( reservedate < NOW() )';
2044 my $dbh = C4::Context->dbh();
2045 my @row = $dbh->selectrow_array(
2046 $sql,
2047 undef,
2048 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2051 return @row ? $row[0]+1 : 1;
2054 =head2 IsItemOnHoldAndFound
2056 my $bool = IsItemFoundHold( $itemnumber );
2058 Returns true if the item is currently on hold
2059 and that hold has a non-null found status ( W, T, etc. )
2061 =cut
2063 sub IsItemOnHoldAndFound {
2064 my ($itemnumber) = @_;
2066 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2068 my $found = $rs->count(
2070 itemnumber => $itemnumber,
2071 found => { '!=' => undef }
2075 return $found;
2078 =head2 GetMaxPatronHoldsForRecord
2080 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2082 For multiple holds on a given record for a given patron, the max
2083 number of record level holds that a patron can be placed is the highest
2084 value of the holds_per_record rule for each item if the record for that
2085 patron. This subroutine finds and returns the highest holds_per_record
2086 rule value for a given patron id and record id.
2088 =cut
2090 sub GetMaxPatronHoldsForRecord {
2091 my ( $borrowernumber, $biblionumber ) = @_;
2093 my $patron = Koha::Patrons->find($borrowernumber);
2094 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2096 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2098 my $categorycode = $patron->categorycode;
2099 my $branchcode;
2100 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2102 my $max = 0;
2103 foreach my $item (@items) {
2104 my $itemtype = $item->effective_itemtype();
2106 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2108 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2109 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2110 $max = $holds_per_record if $holds_per_record > $max;
2113 return $max;
2116 =head2 GetHoldRule
2118 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2120 Returns the matching hold related issuingrule fields for a given
2121 patron category, itemtype, and library.
2123 =cut
2125 sub GetHoldRule {
2126 my ( $categorycode, $itemtype, $branchcode ) = @_;
2128 my $dbh = C4::Context->dbh;
2130 my $sth = $dbh->prepare(
2132 SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record
2133 FROM issuingrules
2134 WHERE (categorycode in (?,'*') )
2135 AND (itemtype IN (?,'*'))
2136 AND (branchcode IN (?,'*'))
2137 ORDER BY categorycode DESC,
2138 itemtype DESC,
2139 branchcode DESC
2143 $sth->execute( $categorycode, $itemtype, $branchcode );
2145 return $sth->fetchrow_hashref();
2148 =head1 AUTHOR
2150 Koha Development Team <http://koha-community.org/>
2152 =cut