Bug 24316: (follow up) Fix es-ES web installer
[koha.git] / C4 / Reserves.pm
blob7a3785724a4836ec971d3ce30e240c685ec36655
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
27 use C4::Accounts;
28 use C4::Biblio;
29 use C4::Circulation;
30 use C4::Context;
31 use C4::Items;
32 use C4::Letters;
33 use C4::Log;
34 use C4::Members::Messaging;
35 use C4::Members;
36 use Koha::Account::Lines;
37 use Koha::Biblios;
38 use Koha::Calendar;
39 use Koha::CirculationRules;
40 use Koha::Database;
41 use Koha::DateUtils;
42 use Koha::Hold;
43 use Koha::Holds;
44 use Koha::IssuingRules;
45 use Koha::ItemTypes;
46 use Koha::Items;
47 use Koha::Libraries;
48 use Koha::Old::Hold;
49 use Koha::Patrons;
51 use Carp;
52 use Data::Dumper;
53 use List::MoreUtils qw( firstidx any );
55 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
57 =head1 NAME
59 C4::Reserves - Koha functions for dealing with reservation.
61 =head1 SYNOPSIS
63 use C4::Reserves;
65 =head1 DESCRIPTION
67 This modules provides somes functions to deal with reservations.
69 Reserves are stored in reserves table.
70 The following columns contains important values :
71 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
72 =0 : then the reserve is being dealed
73 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
74 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
75 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
76 F(inished) : the reserve has been completed, and is done
77 - itemnumber : empty : the reserve is still unaffected to an item
78 filled: the reserve is attached to an item
79 The complete workflow is :
80 ==== 1st use case ====
81 patron request a document, 1st available : P >0, F=NULL, I=NULL
82 a library having it run "transfertodo", and clic on the list
83 if there is no transfer to do, the reserve waiting
84 patron can pick it up P =0, F=W, I=filled
85 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
86 The pickup library receive the book, it check in P =0, F=W, I=filled
87 The patron borrow the book P =0, F=F, I=filled
89 ==== 2nd use case ====
90 patron requests a document, a given item,
91 If pickup is holding branch P =0, F=W, I=filled
92 If transfer needed, write in branchtransfer P =0, F=T, I=filled
93 The pickup library receive the book, it checks it in P =0, F=W, I=filled
94 The patron borrow the book P =0, F=F, I=filled
96 =head1 FUNCTIONS
98 =cut
100 BEGIN {
101 require Exporter;
102 @ISA = qw(Exporter);
103 @EXPORT = qw(
104 &AddReserve
106 &GetReserveStatus
108 &GetOtherReserves
110 &ModReserveFill
111 &ModReserveAffect
112 &ModReserve
113 &ModReserveStatus
114 &ModReserveCancelAll
115 &ModReserveMinusPriority
116 &MoveReserve
118 &CheckReserves
119 &CanBookBeReserved
120 &CanItemBeReserved
121 &CanReserveBeCanceledFromOpac
122 &CancelExpiredReserves
124 &AutoUnsuspendReserves
126 &IsAvailableForItemLevelRequest
128 &AlterPriority
129 &ToggleLowestPriority
131 &ReserveSlip
132 &ToggleSuspend
133 &SuspendAll
135 &GetReservesControlBranch
137 IsItemOnHoldAndFound
139 GetMaxPatronHoldsForRecord
141 @EXPORT_OK = qw( MergeHolds );
144 =head2 AddReserve
146 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
148 Adds reserve and generates HOLDPLACED message.
150 The following tables are available witin the HOLDPLACED message:
152 branches
153 borrowers
154 biblio
155 biblioitems
156 items
157 reserves
159 =cut
161 sub AddReserve {
162 my (
163 $branch, $borrowernumber, $biblionumber, $bibitems,
164 $priority, $resdate, $expdate, $notes,
165 $title, $checkitem, $found, $itemtype
166 ) = @_;
168 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
169 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
171 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
173 # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
174 # of the document, we force the value $priority and $found .
175 if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
176 my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
178 if (
179 # If item is already checked out, it cannot be set waiting
180 !$item->onloan
182 # The item can't be waiting if it needs a transfer
183 && $item->holdingbranch eq $branch
185 # Similarly, if in transit it can't be waiting
186 && !$item->get_transfer
188 # If we can't hold damaged items, and it is damaged, it can't be waiting
189 && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
191 # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
192 && !$item->current_holds->count )
194 $priority = 0;
195 $found = 'W';
199 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
201 # Make room in reserves for this before those of a later reserve date
202 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
205 my $waitingdate;
207 # If the reserv had the waiting status, we had the value of the resdate
208 if ( $found eq 'W' ) {
209 $waitingdate = $resdate;
212 # Don't add itemtype limit if specific item is selected
213 $itemtype = undef if $checkitem;
215 # updates take place here
216 my $hold = Koha::Hold->new(
218 borrowernumber => $borrowernumber,
219 biblionumber => $biblionumber,
220 reservedate => $resdate,
221 branchcode => $branch,
222 priority => $priority,
223 reservenotes => $notes,
224 itemnumber => $checkitem,
225 found => $found,
226 waitingdate => $waitingdate,
227 expirationdate => $expdate,
228 itemtype => $itemtype,
229 item_level_hold => $checkitem ? 1 : 0,
231 )->store();
232 $hold->set_waiting() if $found eq 'W';
234 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
235 if C4::Context->preference('HoldsLog');
237 my $reserve_id = $hold->id();
239 # add a reserve fee if needed
240 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
241 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
242 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
245 _FixPriority({ biblionumber => $biblionumber});
247 # Send e-mail to librarian if syspref is active
248 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
249 my $patron = Koha::Patrons->find( $borrowernumber );
250 my $library = $patron->library;
251 if ( my $letter = C4::Letters::GetPreparedLetter (
252 module => 'reserves',
253 letter_code => 'HOLDPLACED',
254 branchcode => $branch,
255 lang => $patron->lang,
256 tables => {
257 'branches' => $library->unblessed,
258 'borrowers' => $patron->unblessed,
259 'biblio' => $biblionumber,
260 'biblioitems' => $biblionumber,
261 'items' => $checkitem,
262 'reserves' => $hold->unblessed,
264 ) ) {
266 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
268 C4::Letters::EnqueueLetter(
269 { letter => $letter,
270 borrowernumber => $borrowernumber,
271 message_transport_type => 'email',
272 from_address => $admin_email_address,
273 to_address => $admin_email_address,
279 return $reserve_id;
282 =head2 CanBookBeReserved
284 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
285 if ($canReserve eq 'OK') { #We can reserve this Item! }
287 See CanItemBeReserved() for possible return values.
289 =cut
291 sub CanBookBeReserved{
292 my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
294 my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
295 #get items linked via host records
296 my @hostitems = get_hostitemnumbers_of($biblionumber);
297 if (@hostitems){
298 push (@itemnumbers, @hostitems);
301 my $canReserve = { status => '' };
302 foreach my $itemnumber (@itemnumbers) {
303 $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
304 return { status => 'OK' } if $canReserve->{status} eq 'OK';
306 return $canReserve;
309 =head2 CanItemBeReserved
311 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
312 if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
314 @RETURNS { status => OK }, if the Item can be reserved.
315 { status => ageRestricted }, if the Item is age restricted for this borrower.
316 { status => damaged }, if the Item is damaged.
317 { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
318 { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
319 { status => notReservable }, if holds on this item are not allowed
320 { status => libraryNotFound }, if given branchcode is not an existing library
321 { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location
322 { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
324 =cut
326 sub CanItemBeReserved {
327 my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
329 my $dbh = C4::Context->dbh;
330 my $ruleitemtype; # itemtype of the matching issuing rule
331 my $allowedreserves = 0; # Total number of holds allowed across all records
332 my $holds_per_record = 1; # Total number of holds allowed for this one given record
333 my $holds_per_day; # Default to unlimited
335 # we retrieve borrowers and items informations #
336 # item->{itype} will come for biblioitems if necessery
337 my $item = Koha::Items->find($itemnumber);
338 my $biblio = $item->biblio;
339 my $patron = Koha::Patrons->find( $borrowernumber );
340 my $borrower = $patron->unblessed;
342 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
343 return { status =>'damaged' }
344 if ( $item->damaged
345 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
347 # Check for the age restriction
348 my ( $ageRestriction, $daysToAgeRestriction ) =
349 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
350 return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
352 # Check that the patron doesn't have an item level hold on this item already
353 return { status =>'itemAlreadyOnHold' }
354 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
356 my $controlbranch = C4::Context->preference('ReservesControlBranch');
358 my $querycount = q{
359 SELECT count(*) AS count
360 FROM reserves
361 LEFT JOIN items USING (itemnumber)
362 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
363 LEFT JOIN borrowers USING (borrowernumber)
364 WHERE borrowernumber = ?
367 my $branchcode = "";
368 my $branchfield = "reserves.branchcode";
370 if ( $controlbranch eq "ItemHomeLibrary" ) {
371 $branchfield = "items.homebranch";
372 $branchcode = $item->homebranch;
374 elsif ( $controlbranch eq "PatronLibrary" ) {
375 $branchfield = "borrowers.branchcode";
376 $branchcode = $borrower->{branchcode};
379 # we retrieve rights
380 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->effective_itemtype, $branchcode ) ) {
381 $ruleitemtype = $rights->{itemtype};
382 $allowedreserves = $rights->{reservesallowed};
383 $holds_per_record = $rights->{holds_per_record};
384 $holds_per_day = $rights->{holds_per_day};
386 else {
387 $ruleitemtype = '*';
390 my $holds = Koha::Holds->search(
392 borrowernumber => $borrowernumber,
393 biblionumber => $item->biblionumber,
394 found => undef, # Found holds don't count against a patron's holds limit
397 if ( $holds->count() >= $holds_per_record ) {
398 return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
401 my $today_holds = Koha::Holds->search({
402 borrowernumber => $borrowernumber,
403 reservedate => dt_from_string->date
406 if ( defined $holds_per_day &&
407 ( ( $holds_per_day > 0 && $today_holds->count() >= $holds_per_day )
408 or ( $holds_per_day == 0 ) )
410 return { status => 'tooManyReservesToday', limit => $holds_per_day };
413 # we retrieve count
415 $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
417 # If using item-level itypes, fall back to the record
418 # level itemtype if the hold has no associated item
419 $querycount .=
420 C4::Context->preference('item-level_itypes')
421 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
422 : " AND biblioitems.itemtype = ?"
423 if ( $ruleitemtype ne "*" );
425 my $sthcount = $dbh->prepare($querycount);
427 if ( $ruleitemtype eq "*" ) {
428 $sthcount->execute( $borrowernumber, $branchcode );
430 else {
431 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
434 my $reservecount = "0";
435 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
436 $reservecount = $rowcount->{count};
439 # we check if it's ok or not
440 if ( $reservecount >= $allowedreserves ) {
441 return { status => 'tooManyReserves', limit => $allowedreserves };
444 # Now we need to check hold limits by patron category
445 my $rule = Koha::CirculationRules->get_effective_rule(
447 categorycode => $borrower->{categorycode},
448 branchcode => $branchcode,
449 rule_name => 'max_holds',
452 if ( $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
453 my $total_holds_count = Koha::Holds->search(
455 borrowernumber => $borrower->{borrowernumber}
457 )->count();
459 return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
462 my $reserves_control_branch =
463 GetReservesControlBranch( $item->unblessed(), $borrower );
464 my $branchitemrule =
465 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype ); # FIXME Should not be item->effective_itemtype?
467 if ( $branchitemrule->{holdallowed} == 0 ) {
468 return { status => 'notReservable' };
471 if ( $branchitemrule->{holdallowed} == 1
472 && $borrower->{branchcode} ne $item->homebranch )
474 return { status => 'cannotReserveFromOtherBranches' };
477 # If reservecount is ok, we check item branch if IndependentBranches is ON
478 # and canreservefromotherbranches is OFF
479 if ( C4::Context->preference('IndependentBranches')
480 and !C4::Context->preference('canreservefromotherbranches') )
482 if ( $item->homebranch ne $borrower->{branchcode} ) {
483 return { status => 'cannotReserveFromOtherBranches' };
487 if ($pickup_branchcode) {
488 my $destination = Koha::Libraries->find({
489 branchcode => $pickup_branchcode,
492 unless ($destination) {
493 return { status => 'libraryNotFound' };
495 unless ($destination->pickup_location) {
496 return { status => 'libraryNotPickupLocation' };
498 unless ($item->can_be_transferred({ to => $destination })) {
499 return { status => 'cannotBeTransferred' };
503 return { status => 'OK' };
506 =head2 CanReserveBeCanceledFromOpac
508 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
510 returns 1 if reserve can be cancelled by user from OPAC.
511 First check if reserve belongs to user, next checks if reserve is not in
512 transfer or waiting status
514 =cut
516 sub CanReserveBeCanceledFromOpac {
517 my ($reserve_id, $borrowernumber) = @_;
519 return unless $reserve_id and $borrowernumber;
520 my $reserve = Koha::Holds->find($reserve_id);
522 return 0 unless $reserve->borrowernumber == $borrowernumber;
523 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
525 return 1;
529 =head2 GetOtherReserves
531 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
533 Check queued list of this document and check if this document must be transferred
535 =cut
537 sub GetOtherReserves {
538 my ($itemnumber) = @_;
539 my $messages;
540 my $nextreservinfo;
541 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
542 if ($checkreserves) {
543 my $item = Koha::Items->find($itemnumber);
544 if ( $item->holdingbranch ne $checkreserves->{'branchcode'} ) {
545 $messages->{'transfert'} = $checkreserves->{'branchcode'};
546 #minus priorities of others reservs
547 ModReserveMinusPriority(
548 $itemnumber,
549 $checkreserves->{'reserve_id'},
552 #launch the subroutine dotransfer
553 C4::Items::ModItemTransfer(
554 $itemnumber,
555 $item->holdingbranch,
556 $checkreserves->{'branchcode'}
561 #step 2b : case of a reservation on the same branch, set the waiting status
562 else {
563 $messages->{'waiting'} = 1;
564 ModReserveMinusPriority(
565 $itemnumber,
566 $checkreserves->{'reserve_id'},
568 ModReserveStatus($itemnumber,'W');
571 $nextreservinfo = $checkreserves->{'borrowernumber'};
574 return ( $messages, $nextreservinfo );
577 =head2 ChargeReserveFee
579 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
581 Charge the fee for a reserve (if $fee > 0)
583 =cut
585 sub ChargeReserveFee {
586 my ( $borrowernumber, $fee, $title ) = @_;
587 return if !$fee || $fee == 0; # the last test is needed to include 0.00
588 Koha::Account->new( { patron_id => $borrowernumber } )->add_debit(
590 amount => $fee,
591 description => $title,
592 note => undef,
593 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
594 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
595 interface => C4::Context->interface,
596 invoice_type => undef,
597 type => 'RESERVE',
598 item_id => undef
603 =head2 GetReserveFee
605 $fee = GetReserveFee( $borrowernumber, $biblionumber );
607 Calculate the fee for a reserve (if applicable).
609 =cut
611 sub GetReserveFee {
612 my ( $borrowernumber, $biblionumber ) = @_;
613 my $borquery = qq{
614 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
616 my $issue_qry = qq{
617 SELECT COUNT(*) FROM items
618 LEFT JOIN issues USING (itemnumber)
619 WHERE items.biblionumber=? AND issues.issue_id IS NULL
621 my $holds_qry = qq{
622 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
625 my $dbh = C4::Context->dbh;
626 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
627 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
628 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
629 # This is a reconstruction of the old code:
630 # Compare number of items with items issued, and optionally check holds
631 # If not all items are issued and there are no holds: charge no fee
632 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
633 my ( $notissued, $reserved );
634 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
635 ( $biblionumber ) );
636 if( $notissued ) {
637 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
638 ( $biblionumber, $borrowernumber ) );
639 $fee = 0 if $reserved == 0;
642 return $fee;
645 =head2 GetReserveStatus
647 $reservestatus = GetReserveStatus($itemnumber);
649 Takes an itemnumber and returns the status of the reserve placed on it.
650 If several reserves exist, the reserve with the lower priority is given.
652 =cut
654 ## FIXME: I don't think this does what it thinks it does.
655 ## It only ever checks the first reserve result, even though
656 ## multiple reserves for that bib can have the itemnumber set
657 ## the sub is only used once in the codebase.
658 sub GetReserveStatus {
659 my ($itemnumber) = @_;
661 my $dbh = C4::Context->dbh;
663 my ($sth, $found, $priority);
664 if ( $itemnumber ) {
665 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
666 $sth->execute($itemnumber);
667 ($found, $priority) = $sth->fetchrow_array;
670 if(defined $found) {
671 return 'Waiting' if $found eq 'W' and $priority == 0;
672 return 'Finished' if $found eq 'F';
675 return 'Reserved' if $priority > 0;
677 return ''; # empty string here will remove need for checking undef, or less log lines
680 =head2 CheckReserves
682 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber);
683 ($status, $matched_reserve, $possible_reserves) = &CheckReserves(undef, $barcode);
684 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
686 Find a book in the reserves.
688 C<$itemnumber> is the book's item number.
689 C<$lookahead> is the number of days to look in advance for future reserves.
691 As I understand it, C<&CheckReserves> looks for the given item in the
692 reserves. If it is found, that's a match, and C<$status> is set to
693 C<Waiting>.
695 Otherwise, it finds the most important item in the reserves with the
696 same biblio number as this book (I'm not clear on this) and returns it
697 with C<$status> set to C<Reserved>.
699 C<&CheckReserves> returns a two-element list:
701 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
703 C<$reserve> is the reserve item that matched. It is a
704 reference-to-hash whose keys are mostly the fields of the reserves
705 table in the Koha database.
707 =cut
709 sub CheckReserves {
710 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
711 my $dbh = C4::Context->dbh;
712 my $sth;
713 my $select;
714 if (C4::Context->preference('item-level_itypes')){
715 $select = "
716 SELECT items.biblionumber,
717 items.biblioitemnumber,
718 itemtypes.notforloan,
719 items.notforloan AS itemnotforloan,
720 items.itemnumber,
721 items.damaged,
722 items.homebranch,
723 items.holdingbranch
724 FROM items
725 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
726 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
729 else {
730 $select = "
731 SELECT items.biblionumber,
732 items.biblioitemnumber,
733 itemtypes.notforloan,
734 items.notforloan AS itemnotforloan,
735 items.itemnumber,
736 items.damaged,
737 items.homebranch,
738 items.holdingbranch
739 FROM items
740 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
741 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
745 if ($item) {
746 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
747 $sth->execute($item);
749 else {
750 $sth = $dbh->prepare("$select WHERE barcode = ?");
751 $sth->execute($barcode);
753 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
754 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
755 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
757 return unless $itemnumber; # bail if we got nothing.
758 # if item is not for loan it cannot be reserved either.....
759 # except where items.notforloan < 0 : This indicates the item is holdable.
760 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
762 # Find this item in the reserves
763 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
765 # $priority and $highest are used to find the most important item
766 # in the list returned by &_Findgroupreserve. (The lower $priority,
767 # the more important the item.)
768 # $highest is the most important item we've seen so far.
769 my $highest;
770 if (scalar @reserves) {
771 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
772 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
773 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
775 my $priority = 10000000;
776 foreach my $res (@reserves) {
777 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
778 if ($res->{'found'} eq 'W') {
779 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
780 } else {
781 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
783 } else {
784 my $patron;
785 my $item;
786 my $local_hold_match;
788 if ($LocalHoldsPriority) {
789 $patron = Koha::Patrons->find( $res->{borrowernumber} );
790 $item = Koha::Items->find($itemnumber);
792 my $local_holds_priority_item_branchcode =
793 $item->$LocalHoldsPriorityItemControl;
794 my $local_holds_priority_patron_branchcode =
795 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
796 ? $res->{branchcode}
797 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
798 ? $patron->branchcode
799 : undef;
800 $local_hold_match =
801 $local_holds_priority_item_branchcode eq
802 $local_holds_priority_patron_branchcode;
805 # See if this item is more important than what we've got so far
806 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
807 $item ||= Koha::Items->find($itemnumber);
808 next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
809 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
810 my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
811 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
812 next if ($branchitemrule->{'holdallowed'} == 0);
813 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
814 my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
815 next
816 if $hold_fulfillment_policy ne 'any'
817 && (
818 $hold_fulfillment_policy eq ''
819 || ( $res->{branchcode} ne
820 $item->$hold_fulfillment_policy )
822 next unless $item->can_be_transferred( { to => scalar Koha::Libraries->find( $res->{branchcode} ) } );
823 $priority = $res->{'priority'};
824 $highest = $res;
825 last if $local_hold_match;
831 # If we get this far, then no exact match was found.
832 # We return the most important (i.e. next) reservation.
833 if ($highest) {
834 $highest->{'itemnumber'} = $item;
835 return ( "Reserved", $highest, \@reserves );
838 return ( '' );
841 =head2 CancelExpiredReserves
843 CancelExpiredReserves();
845 Cancels all reserves with an expiration date from before today.
847 =cut
849 sub CancelExpiredReserves {
850 my $today = dt_from_string();
851 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
852 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
854 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
855 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
856 $params->{found} = [ { '!=', 'W' }, undef ] unless $expireWaiting;
858 # FIXME To move to Koha::Holds->search_expired (?)
859 my $holds = Koha::Holds->search( $params );
861 while ( my $hold = $holds->next ) {
862 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
864 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
866 my $cancel_params = {};
867 if ( $hold->found eq 'W' ) {
868 $cancel_params->{charge_cancel_fee} = 1;
870 $hold->cancel( $cancel_params );
874 =head2 AutoUnsuspendReserves
876 AutoUnsuspendReserves();
878 Unsuspends all suspended reserves with a suspend_until date from before today.
880 =cut
882 sub AutoUnsuspendReserves {
883 my $today = dt_from_string();
885 my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
887 map { $_->resume() } @holds;
890 =head2 ModReserve
892 ModReserve({ rank => $rank,
893 reserve_id => $reserve_id,
894 branchcode => $branchcode
895 [, itemnumber => $itemnumber ]
896 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
899 Change a hold request's priority or cancel it.
901 C<$rank> specifies the effect of the change. If C<$rank>
902 is 'W' or 'n', nothing happens. This corresponds to leaving a
903 request alone when changing its priority in the holds queue
904 for a bib.
906 If C<$rank> is 'del', the hold request is cancelled.
908 If C<$rank> is an integer greater than zero, the priority of
909 the request is set to that value. Since priority != 0 means
910 that the item is not waiting on the hold shelf, setting the
911 priority to a non-zero value also sets the request's found
912 status and waiting date to NULL.
914 The optional C<$itemnumber> parameter is used only when
915 C<$rank> is a non-zero integer; if supplied, the itemnumber
916 of the hold request is set accordingly; if omitted, the itemnumber
917 is cleared.
919 B<FIXME:> Note that the forgoing can have the effect of causing
920 item-level hold requests to turn into title-level requests. This
921 will be fixed once reserves has separate columns for requested
922 itemnumber and supplying itemnumber.
924 =cut
926 sub ModReserve {
927 my ( $params ) = @_;
929 my $rank = $params->{'rank'};
930 my $reserve_id = $params->{'reserve_id'};
931 my $branchcode = $params->{'branchcode'};
932 my $itemnumber = $params->{'itemnumber'};
933 my $suspend_until = $params->{'suspend_until'};
934 my $borrowernumber = $params->{'borrowernumber'};
935 my $biblionumber = $params->{'biblionumber'};
937 return if $rank eq "W";
938 return if $rank eq "n";
940 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
942 my $hold;
943 unless ( $reserve_id ) {
944 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
945 return unless $holds->count; # FIXME Should raise an exception
946 $hold = $holds->next;
947 $reserve_id = $hold->reserve_id;
950 $hold ||= Koha::Holds->find($reserve_id);
952 if ( $rank eq "del" ) {
953 $hold->cancel;
955 elsif ($rank =~ /^\d+/ and $rank > 0) {
956 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
957 if C4::Context->preference('HoldsLog');
959 my $properties = {
960 priority => $rank,
961 branchcode => $branchcode,
962 itemnumber => $itemnumber,
963 found => undef,
964 waitingdate => undef
966 if (exists $params->{reservedate}) {
967 $properties->{reservedate} = $params->{reservedate} || undef;
969 if (exists $params->{expirationdate}) {
970 $properties->{expirationdate} = $params->{expirationdate} || undef;
973 $hold->set($properties)->store();
975 if ( defined( $suspend_until ) ) {
976 if ( $suspend_until ) {
977 $suspend_until = eval { dt_from_string( $suspend_until ) };
978 $hold->suspend_hold( $suspend_until );
979 } else {
980 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
981 # If the hold is not suspended, this does nothing.
982 $hold->set( { suspend_until => undef } )->store();
986 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
990 =head2 ModReserveFill
992 &ModReserveFill($reserve);
994 Fill a reserve. If I understand this correctly, this means that the
995 reserved book has been found and given to the patron who reserved it.
997 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
998 whose keys are fields from the reserves table in the Koha database.
1000 =cut
1002 sub ModReserveFill {
1003 my ($res) = @_;
1004 my $reserve_id = $res->{'reserve_id'};
1006 my $hold = Koha::Holds->find($reserve_id);
1007 # get the priority on this record....
1008 my $priority = $hold->priority;
1010 # update the hold statuses, no need to store it though, we will be deleting it anyway
1011 $hold->set(
1013 found => 'F',
1014 priority => 0,
1018 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
1019 Koha::Old::Hold->new( $hold->unblessed() )->store();
1021 $hold->delete();
1023 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1024 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1025 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1028 # now fix the priority on the others (if the priority wasn't
1029 # already sorted!)....
1030 unless ( $priority == 0 ) {
1031 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1035 =head2 ModReserveStatus
1037 &ModReserveStatus($itemnumber, $newstatus);
1039 Update the reserve status for the active (priority=0) reserve.
1041 $itemnumber is the itemnumber the reserve is on
1043 $newstatus is the new status.
1045 =cut
1047 sub ModReserveStatus {
1049 #first : check if we have a reservation for this item .
1050 my ($itemnumber, $newstatus) = @_;
1051 my $dbh = C4::Context->dbh;
1053 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1054 my $sth_set = $dbh->prepare($query);
1055 $sth_set->execute( $newstatus, $itemnumber );
1057 my $item = Koha::Items->find($itemnumber);
1058 if ( ( $item->location eq 'CART' && $item->permanent_location ne 'CART' ) && $newstatus ) {
1059 CartToShelf( $itemnumber );
1063 =head2 ModReserveAffect
1065 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1067 This function affect an item and a status for a given reserve, either fetched directly
1068 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1069 is given, only first reserve returned is affected, which is ok for anything but
1070 multi-item holds.
1072 if $transferToDo is not set, then the status is set to "Waiting" as well.
1073 otherwise, a transfer is on the way, and the end of the transfer will
1074 take care of the waiting status
1076 =cut
1078 sub ModReserveAffect {
1079 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1080 my $dbh = C4::Context->dbh;
1082 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1083 # attached to $itemnumber
1084 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1085 $sth->execute($itemnumber);
1086 my ($biblionumber) = $sth->fetchrow;
1088 # get request - need to find out if item is already
1089 # waiting in order to not send duplicate hold filled notifications
1091 my $hold;
1092 # Find hold by id if we have it
1093 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1094 # Find item level hold for this item if there is one
1095 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1096 # Find record level hold if there is no item level hold
1097 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1099 return unless $hold;
1101 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1103 $hold->itemnumber($itemnumber);
1104 $hold->set_waiting($transferToDo);
1106 _koha_notify_reserve( $hold->reserve_id )
1107 if ( !$transferToDo && !$already_on_shelf );
1109 _FixPriority( { biblionumber => $biblionumber } );
1110 my $item = Koha::Items->find($itemnumber);
1111 if ( ( $item->location eq 'CART' && $item->permanent_location ne 'CART' ) ) {
1112 CartToShelf( $itemnumber );
1115 return;
1118 =head2 ModReserveCancelAll
1120 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1122 function to cancel reserv,check other reserves, and transfer document if it's necessary
1124 =cut
1126 sub ModReserveCancelAll {
1127 my $messages;
1128 my $nextreservinfo;
1129 my ( $itemnumber, $borrowernumber ) = @_;
1131 #step 1 : cancel the reservation
1132 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1133 return unless $holds->count;
1134 $holds->next->cancel;
1136 #step 2 launch the subroutine of the others reserves
1137 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1139 return ( $messages, $nextreservinfo );
1142 =head2 ModReserveMinusPriority
1144 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1146 Reduce the values of queued list
1148 =cut
1150 sub ModReserveMinusPriority {
1151 my ( $itemnumber, $reserve_id ) = @_;
1153 #first step update the value of the first person on reserv
1154 my $dbh = C4::Context->dbh;
1155 my $query = "
1156 UPDATE reserves
1157 SET priority = 0 , itemnumber = ?
1158 WHERE reserve_id = ?
1160 my $sth_upd = $dbh->prepare($query);
1161 $sth_upd->execute( $itemnumber, $reserve_id );
1162 # second step update all others reserves
1163 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1166 =head2 IsAvailableForItemLevelRequest
1168 my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1170 Checks whether a given item record is available for an
1171 item-level hold request. An item is available if
1173 * it is not lost AND
1174 * it is not damaged AND
1175 * it is not withdrawn AND
1176 * a waiting or in transit reserve is placed on
1177 * does not have a not for loan value > 0
1179 Need to check the issuingrules onshelfholds column,
1180 if this is set items on the shelf can be placed on hold
1182 Note that IsAvailableForItemLevelRequest() does not
1183 check if the staff operator is authorized to place
1184 a request on the item - in particular,
1185 this routine does not check IndependentBranches
1186 and canreservefromotherbranches.
1188 =cut
1190 sub IsAvailableForItemLevelRequest {
1191 my ( $item, $patron, $pickup_branchcode ) = @_;
1193 my $dbh = C4::Context->dbh;
1194 # must check the notforloan setting of the itemtype
1195 # FIXME - a lot of places in the code do this
1196 # or something similar - need to be
1197 # consolidated
1198 my $itemtype = $item->effective_itemtype;
1199 my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1201 return 0 if
1202 $notforloan_per_itemtype ||
1203 $item->itemlost ||
1204 $item->notforloan > 0 ||
1205 $item->withdrawn ||
1206 ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1208 my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1210 if ($pickup_branchcode) {
1211 my $destination = Koha::Libraries->find($pickup_branchcode);
1212 return 0 unless $destination;
1213 return 0 unless $destination->pickup_location;
1214 return 0 unless $item->can_be_transferred( { to => $destination } );
1217 if ( $on_shelf_holds == 1 ) {
1218 return 1;
1219 } elsif ( $on_shelf_holds == 2 ) {
1220 my @items =
1221 Koha::Items->search( { biblionumber => $item->biblionumber } );
1223 my $any_available = 0;
1225 foreach my $i (@items) {
1226 my $reserves_control_branch = GetReservesControlBranch( $i->unblessed(), $patron->unblessed );
1227 my $branchitemrule = C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1229 $any_available = 1
1230 unless $i->itemlost
1231 || $i->notforloan > 0
1232 || $i->withdrawn
1233 || $i->onloan
1234 || IsItemOnHoldAndFound( $i->id )
1235 || ( $i->damaged
1236 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1237 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1238 || $branchitemrule->{holdallowed} == 1 && $patron->branchcode ne $i->homebranch;
1241 return $any_available ? 0 : 1;
1242 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1243 return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1247 sub _get_itype {
1248 my $item = shift;
1250 my $itype;
1251 if (C4::Context->preference('item-level_itypes')) {
1252 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1253 # When GetItem is fixed, we can remove this
1254 $itype = $item->{itype};
1256 else {
1257 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1258 # So if we already have a biblioitems join when calling this function,
1259 # we don't need to access the database again
1260 $itype = $item->{itemtype};
1262 unless ($itype) {
1263 my $dbh = C4::Context->dbh;
1264 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1265 my $sth = $dbh->prepare($query);
1266 $sth->execute($item->{biblioitemnumber});
1267 if (my $data = $sth->fetchrow_hashref()){
1268 $itype = $data->{itemtype};
1271 return $itype;
1274 =head2 AlterPriority
1276 AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1278 This function changes a reserve's priority up, down, to the top, or to the bottom.
1279 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1281 =cut
1283 sub AlterPriority {
1284 my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1286 my $hold = Koha::Holds->find( $reserve_id );
1287 return unless $hold;
1289 if ( $hold->cancellationdate ) {
1290 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1291 return;
1294 if ( $where eq 'up' ) {
1295 return unless $prev_priority;
1296 _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1297 } elsif ( $where eq 'down' ) {
1298 return unless $next_priority;
1299 _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1300 } elsif ( $where eq 'top' ) {
1301 _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1302 } elsif ( $where eq 'bottom' ) {
1303 _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1306 # FIXME Should return the new priority
1309 =head2 ToggleLowestPriority
1311 ToggleLowestPriority( $borrowernumber, $biblionumber );
1313 This function sets the lowestPriority field to true if is false, and false if it is true.
1315 =cut
1317 sub ToggleLowestPriority {
1318 my ( $reserve_id ) = @_;
1320 my $dbh = C4::Context->dbh;
1322 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1323 $sth->execute( $reserve_id );
1325 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1328 =head2 ToggleSuspend
1330 ToggleSuspend( $reserve_id );
1332 This function sets the suspend field to true if is false, and false if it is true.
1333 If the reserve is currently suspended with a suspend_until date, that date will
1334 be cleared when it is unsuspended.
1336 =cut
1338 sub ToggleSuspend {
1339 my ( $reserve_id, $suspend_until ) = @_;
1341 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1343 my $hold = Koha::Holds->find( $reserve_id );
1345 if ( $hold->is_suspended ) {
1346 $hold->resume()
1347 } else {
1348 $hold->suspend_hold( $suspend_until );
1352 =head2 SuspendAll
1354 SuspendAll(
1355 borrowernumber => $borrowernumber,
1356 [ biblionumber => $biblionumber, ]
1357 [ suspend_until => $suspend_until, ]
1358 [ suspend => $suspend ]
1361 This function accepts a set of hash keys as its parameters.
1362 It requires either borrowernumber or biblionumber, or both.
1364 suspend_until is wholly optional.
1366 =cut
1368 sub SuspendAll {
1369 my %params = @_;
1371 my $borrowernumber = $params{'borrowernumber'} || undef;
1372 my $biblionumber = $params{'biblionumber'} || undef;
1373 my $suspend_until = $params{'suspend_until'} || undef;
1374 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1376 $suspend_until = eval { dt_from_string($suspend_until) }
1377 if ( defined($suspend_until) );
1379 return unless ( $borrowernumber || $biblionumber );
1381 my $params;
1382 $params->{found} = undef;
1383 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1384 $params->{biblionumber} = $biblionumber if $biblionumber;
1386 my @holds = Koha::Holds->search($params);
1388 if ($suspend) {
1389 map { $_->suspend_hold($suspend_until) } @holds;
1391 else {
1392 map { $_->resume() } @holds;
1397 =head2 _FixPriority
1399 _FixPriority({
1400 reserve_id => $reserve_id,
1401 [rank => $rank,]
1402 [ignoreSetLowestRank => $ignoreSetLowestRank]
1407 _FixPriority({ biblionumber => $biblionumber});
1409 This routine adjusts the priority of a hold request and holds
1410 on the same bib.
1412 In the first form, where a reserve_id is passed, the priority of the
1413 hold is set to supplied rank, and other holds for that bib are adjusted
1414 accordingly. If the rank is "del", the hold is cancelled. If no rank
1415 is supplied, all of the holds on that bib have their priority adjusted
1416 as if the second form had been used.
1418 In the second form, where a biblionumber is passed, the holds on that
1419 bib (that are not captured) are sorted in order of increasing priority,
1420 then have reserves.priority set so that the first non-captured hold
1421 has its priority set to 1, the second non-captured hold has its priority
1422 set to 2, and so forth.
1424 In both cases, holds that have the lowestPriority flag on are have their
1425 priority adjusted to ensure that they remain at the end of the line.
1427 Note that the ignoreSetLowestRank parameter is meant to be used only
1428 when _FixPriority calls itself.
1430 =cut
1432 sub _FixPriority {
1433 my ( $params ) = @_;
1434 my $reserve_id = $params->{reserve_id};
1435 my $rank = $params->{rank} // '';
1436 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1437 my $biblionumber = $params->{biblionumber};
1439 my $dbh = C4::Context->dbh;
1441 my $hold;
1442 if ( $reserve_id ) {
1443 $hold = Koha::Holds->find( $reserve_id );
1444 return unless $hold;
1447 unless ( $biblionumber ) { # FIXME This is a very weird API
1448 $biblionumber = $hold->biblionumber;
1451 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1452 $hold->cancel;
1454 elsif ( $rank eq "W" || $rank eq "0" ) {
1456 # make sure priority for waiting or in-transit items is 0
1457 my $query = "
1458 UPDATE reserves
1459 SET priority = 0
1460 WHERE reserve_id = ?
1461 AND found IN ('W', 'T')
1463 my $sth = $dbh->prepare($query);
1464 $sth->execute( $reserve_id );
1466 my @priority;
1468 # get whats left
1469 my $query = "
1470 SELECT reserve_id, borrowernumber, reservedate
1471 FROM reserves
1472 WHERE biblionumber = ?
1473 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1474 ORDER BY priority ASC
1476 my $sth = $dbh->prepare($query);
1477 $sth->execute( $biblionumber );
1478 while ( my $line = $sth->fetchrow_hashref ) {
1479 push( @priority, $line );
1482 # To find the matching index
1483 my $i;
1484 my $key = -1; # to allow for 0 to be a valid result
1485 for ( $i = 0 ; $i < @priority ; $i++ ) {
1486 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1487 $key = $i; # save the index
1488 last;
1492 # if index exists in array then move it to new position
1493 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1494 my $new_rank = $rank -
1495 1; # $new_rank is what you want the new index to be in the array
1496 my $moving_item = splice( @priority, $key, 1 );
1497 splice( @priority, $new_rank, 0, $moving_item );
1500 # now fix the priority on those that are left....
1501 $query = "
1502 UPDATE reserves
1503 SET priority = ?
1504 WHERE reserve_id = ?
1506 $sth = $dbh->prepare($query);
1507 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1508 $sth->execute(
1509 $j + 1,
1510 $priority[$j]->{'reserve_id'}
1514 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1515 $sth->execute();
1517 unless ( $ignoreSetLowestRank ) {
1518 while ( my $res = $sth->fetchrow_hashref() ) {
1519 _FixPriority({
1520 reserve_id => $res->{'reserve_id'},
1521 rank => '999999',
1522 ignoreSetLowestRank => 1
1528 =head2 _Findgroupreserve
1530 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1532 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1533 first match found. If neither, then we look for non-holds-queue based holds.
1534 Lookahead is the number of days to look in advance.
1536 C<&_Findgroupreserve> returns :
1537 C<@results> is an array of references-to-hash whose keys are mostly
1538 fields from the reserves table of the Koha database, plus
1539 C<biblioitemnumber>.
1541 This routine with either return:
1542 1 - Item specific holds from the holds queue
1543 2 - Title level holds from the holds queue
1544 3 - All holds for this biblionumber
1546 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1548 =cut
1550 sub _Findgroupreserve {
1551 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1552 my $dbh = C4::Context->dbh;
1554 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1555 # check for exact targeted match
1556 my $item_level_target_query = qq{
1557 SELECT reserves.biblionumber AS biblionumber,
1558 reserves.borrowernumber AS borrowernumber,
1559 reserves.reservedate AS reservedate,
1560 reserves.branchcode AS branchcode,
1561 reserves.cancellationdate AS cancellationdate,
1562 reserves.found AS found,
1563 reserves.reservenotes AS reservenotes,
1564 reserves.priority AS priority,
1565 reserves.timestamp AS timestamp,
1566 biblioitems.biblioitemnumber AS biblioitemnumber,
1567 reserves.itemnumber AS itemnumber,
1568 reserves.reserve_id AS reserve_id,
1569 reserves.itemtype AS itemtype
1570 FROM reserves
1571 JOIN biblioitems USING (biblionumber)
1572 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1573 WHERE found IS NULL
1574 AND priority > 0
1575 AND item_level_request = 1
1576 AND itemnumber = ?
1577 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1578 AND suspend = 0
1579 ORDER BY priority
1581 my $sth = $dbh->prepare($item_level_target_query);
1582 $sth->execute($itemnumber, $lookahead||0);
1583 my @results;
1584 if ( my $data = $sth->fetchrow_hashref ) {
1585 push( @results, $data )
1586 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1588 return @results if @results;
1590 # check for title-level targeted match
1591 my $title_level_target_query = qq{
1592 SELECT reserves.biblionumber AS biblionumber,
1593 reserves.borrowernumber AS borrowernumber,
1594 reserves.reservedate AS reservedate,
1595 reserves.branchcode AS branchcode,
1596 reserves.cancellationdate AS cancellationdate,
1597 reserves.found AS found,
1598 reserves.reservenotes AS reservenotes,
1599 reserves.priority AS priority,
1600 reserves.timestamp AS timestamp,
1601 biblioitems.biblioitemnumber AS biblioitemnumber,
1602 reserves.itemnumber AS itemnumber,
1603 reserves.reserve_id AS reserve_id,
1604 reserves.itemtype AS itemtype
1605 FROM reserves
1606 JOIN biblioitems USING (biblionumber)
1607 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1608 WHERE found IS NULL
1609 AND priority > 0
1610 AND item_level_request = 0
1611 AND hold_fill_targets.itemnumber = ?
1612 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1613 AND suspend = 0
1614 ORDER BY priority
1616 $sth = $dbh->prepare($title_level_target_query);
1617 $sth->execute($itemnumber, $lookahead||0);
1618 @results = ();
1619 if ( my $data = $sth->fetchrow_hashref ) {
1620 push( @results, $data )
1621 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1623 return @results if @results;
1625 my $query = qq{
1626 SELECT reserves.biblionumber AS biblionumber,
1627 reserves.borrowernumber AS borrowernumber,
1628 reserves.reservedate AS reservedate,
1629 reserves.waitingdate AS waitingdate,
1630 reserves.branchcode AS branchcode,
1631 reserves.cancellationdate AS cancellationdate,
1632 reserves.found AS found,
1633 reserves.reservenotes AS reservenotes,
1634 reserves.priority AS priority,
1635 reserves.timestamp AS timestamp,
1636 reserves.itemnumber AS itemnumber,
1637 reserves.reserve_id AS reserve_id,
1638 reserves.itemtype AS itemtype
1639 FROM reserves
1640 WHERE reserves.biblionumber = ?
1641 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1642 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1643 AND suspend = 0
1644 ORDER BY priority
1646 $sth = $dbh->prepare($query);
1647 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1648 @results = ();
1649 while ( my $data = $sth->fetchrow_hashref ) {
1650 push( @results, $data )
1651 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1653 return @results;
1656 =head2 _koha_notify_reserve
1658 _koha_notify_reserve( $hold->reserve_id );
1660 Sends a notification to the patron that their hold has been filled (through
1661 ModReserveAffect, _not_ ModReserveFill)
1663 The letter code for this notice may be found using the following query:
1665 select distinct letter_code
1666 from message_transports
1667 inner join message_attributes using (message_attribute_id)
1668 where message_name = 'Hold_Filled'
1670 This will probably sipmly be 'HOLD', but because it is defined in the database,
1671 it is subject to addition or change.
1673 The following tables are availalbe witin the notice:
1675 branches
1676 borrowers
1677 biblio
1678 biblioitems
1679 reserves
1680 items
1682 =cut
1684 sub _koha_notify_reserve {
1685 my $reserve_id = shift;
1686 my $hold = Koha::Holds->find($reserve_id);
1687 my $borrowernumber = $hold->borrowernumber;
1689 my $patron = Koha::Patrons->find( $borrowernumber );
1691 # Try to get the borrower's email address
1692 my $to_address = $patron->notice_email_address;
1694 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1695 borrowernumber => $borrowernumber,
1696 message_name => 'Hold_Filled'
1697 } );
1699 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1701 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1703 my %letter_params = (
1704 module => 'reserves',
1705 branchcode => $hold->branchcode,
1706 lang => $patron->lang,
1707 tables => {
1708 'branches' => $library,
1709 'borrowers' => $patron->unblessed,
1710 'biblio' => $hold->biblionumber,
1711 'biblioitems' => $hold->biblionumber,
1712 'reserves' => $hold->unblessed,
1713 'items' => $hold->itemnumber,
1717 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.
1718 my $send_notification = sub {
1719 my ( $mtt, $letter_code ) = (@_);
1720 return unless defined $letter_code;
1721 $letter_params{letter_code} = $letter_code;
1722 $letter_params{message_transport_type} = $mtt;
1723 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1724 unless ($letter) {
1725 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1726 return;
1729 C4::Letters::EnqueueLetter( {
1730 letter => $letter,
1731 borrowernumber => $borrowernumber,
1732 from_address => $admin_email_address,
1733 message_transport_type => $mtt,
1734 } );
1737 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1738 next if (
1739 ( $mtt eq 'email' and not $to_address ) # No email address
1740 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1741 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1744 &$send_notification($mtt, $letter_code);
1745 $notification_sent++;
1747 #Making sure that a print notification is sent if no other transport types can be utilized.
1748 if (! $notification_sent) {
1749 &$send_notification('print', 'HOLD');
1754 =head2 _ShiftPriorityByDateAndPriority
1756 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1758 This increments the priority of all reserves after the one
1759 with either the lowest date after C<$reservedate>
1760 or the lowest priority after C<$priority>.
1762 It effectively makes room for a new reserve to be inserted with a certain
1763 priority, which is returned.
1765 This is most useful when the reservedate can be set by the user. It allows
1766 the new reserve to be placed before other reserves that have a later
1767 reservedate. Since priority also is set by the form in reserves/request.pl
1768 the sub accounts for that too.
1770 =cut
1772 sub _ShiftPriorityByDateAndPriority {
1773 my ( $biblio, $resdate, $new_priority ) = @_;
1775 my $dbh = C4::Context->dbh;
1776 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1777 my $sth = $dbh->prepare( $query );
1778 $sth->execute( $biblio, $resdate, $new_priority );
1779 my $min_priority = $sth->fetchrow;
1780 # if no such matches are found, $new_priority remains as original value
1781 $new_priority = $min_priority if ( $min_priority );
1783 # Shift the priority up by one; works in conjunction with the next SQL statement
1784 $query = "UPDATE reserves
1785 SET priority = priority+1
1786 WHERE biblionumber = ?
1787 AND borrowernumber = ?
1788 AND reservedate = ?
1789 AND found IS NULL";
1790 my $sth_update = $dbh->prepare( $query );
1792 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1793 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1794 $sth = $dbh->prepare( $query );
1795 $sth->execute( $new_priority, $biblio );
1796 while ( my $row = $sth->fetchrow_hashref ) {
1797 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1800 return $new_priority; # so the caller knows what priority they wind up receiving
1803 =head2 MoveReserve
1805 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1807 Use when checking out an item to handle reserves
1808 If $cancelreserve boolean is set to true, it will remove existing reserve
1810 =cut
1812 sub MoveReserve {
1813 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1815 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1816 my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1817 return unless $res;
1819 my $biblionumber = $res->{biblionumber};
1821 if ($res->{borrowernumber} == $borrowernumber) {
1822 ModReserveFill($res);
1824 else {
1825 # warn "Reserved";
1826 # The item is reserved by someone else.
1827 # Find this item in the reserves
1829 my $borr_res = Koha::Holds->search({
1830 borrowernumber => $borrowernumber,
1831 biblionumber => $biblionumber,
1833 order_by => 'priority'
1834 })->next();
1836 if ( $borr_res ) {
1837 # The item is reserved by the current patron
1838 ModReserveFill($borr_res->unblessed);
1841 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1842 RevertWaitingStatus({ itemnumber => $itemnumber });
1844 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1845 my $hold = Koha::Holds->find( $res->{reserve_id} );
1846 $hold->cancel;
1851 =head2 MergeHolds
1853 MergeHolds($dbh,$to_biblio, $from_biblio);
1855 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1857 =cut
1859 sub MergeHolds {
1860 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1861 my $sth = $dbh->prepare(
1862 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1864 $sth->execute($from_biblio);
1865 if ( my $data = $sth->fetchrow_hashref() ) {
1867 # holds exist on old record, if not we don't need to do anything
1868 $sth = $dbh->prepare(
1869 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1870 $sth->execute( $to_biblio, $from_biblio );
1872 # Reorder by date
1873 # don't reorder those already waiting
1875 $sth = $dbh->prepare(
1876 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1878 my $upd_sth = $dbh->prepare(
1879 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1880 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1882 $sth->execute( $to_biblio, 'W', 'T' );
1883 my $priority = 1;
1884 while ( my $reserve = $sth->fetchrow_hashref() ) {
1885 $upd_sth->execute(
1886 $priority, $to_biblio,
1887 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1888 $reserve->{'itemnumber'}
1890 $priority++;
1895 =head2 RevertWaitingStatus
1897 RevertWaitingStatus({ itemnumber => $itemnumber });
1899 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1901 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1902 item level hold, even if it was only a bibliolevel hold to
1903 begin with. This is because we can no longer know if a hold
1904 was item-level or bib-level after a hold has been set to
1905 waiting status.
1907 =cut
1909 sub RevertWaitingStatus {
1910 my ( $params ) = @_;
1911 my $itemnumber = $params->{'itemnumber'};
1913 return unless ( $itemnumber );
1915 my $dbh = C4::Context->dbh;
1917 ## Get the waiting reserve we want to revert
1918 my $query = "
1919 SELECT * FROM reserves
1920 WHERE itemnumber = ?
1921 AND found IS NOT NULL
1923 my $sth = $dbh->prepare( $query );
1924 $sth->execute( $itemnumber );
1925 my $reserve = $sth->fetchrow_hashref();
1927 my $hold = Koha::Holds->find( $reserve->{reserve_id} ); # TODO Remove the next raw SQL statements and use this instead
1929 ## Increment the priority of all other non-waiting
1930 ## reserves for this bib record
1931 $query = "
1932 UPDATE reserves
1934 priority = priority + 1
1935 WHERE
1936 biblionumber = ?
1938 priority > 0
1940 $sth = $dbh->prepare( $query );
1941 $sth->execute( $reserve->{'biblionumber'} );
1943 ## Fix up the currently waiting reserve
1944 $query = "
1945 UPDATE reserves
1947 priority = 1,
1948 found = NULL,
1949 waitingdate = NULL
1950 WHERE
1951 reserve_id = ?
1953 $sth = $dbh->prepare( $query );
1954 $sth->execute( $reserve->{'reserve_id'} );
1956 unless ( $hold->item_level_hold ) {
1957 $hold->itemnumber(undef)->store;
1960 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1963 =head2 ReserveSlip
1965 ReserveSlip(
1967 branchcode => $branchcode,
1968 borrowernumber => $borrowernumber,
1969 biblionumber => $biblionumber,
1970 [ itemnumber => $itemnumber, ]
1971 [ barcode => $barcode, ]
1975 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1977 The letter code will be HOLD_SLIP, and the following tables are
1978 available within the slip:
1980 reserves
1981 branches
1982 borrowers
1983 biblio
1984 biblioitems
1985 items
1987 =cut
1989 sub ReserveSlip {
1990 my ($args) = @_;
1991 my $branchcode = $args->{branchcode};
1992 my $borrowernumber = $args->{borrowernumber};
1993 my $biblionumber = $args->{biblionumber};
1994 my $itemnumber = $args->{itemnumber};
1995 my $barcode = $args->{barcode};
1998 my $patron = Koha::Patrons->find($borrowernumber);
2000 my $hold;
2001 if ($itemnumber || $barcode ) {
2002 $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
2004 $hold = Koha::Holds->search(
2006 biblionumber => $biblionumber,
2007 borrowernumber => $borrowernumber,
2008 itemnumber => $itemnumber
2010 )->next;
2012 else {
2013 $hold = Koha::Holds->search(
2015 biblionumber => $biblionumber,
2016 borrowernumber => $borrowernumber
2018 )->next;
2021 return unless $hold;
2022 my $reserve = $hold->unblessed;
2024 return C4::Letters::GetPreparedLetter (
2025 module => 'circulation',
2026 letter_code => 'HOLD_SLIP',
2027 branchcode => $branchcode,
2028 lang => $patron->lang,
2029 tables => {
2030 'reserves' => $reserve,
2031 'branches' => $reserve->{branchcode},
2032 'borrowers' => $reserve->{borrowernumber},
2033 'biblio' => $reserve->{biblionumber},
2034 'biblioitems' => $reserve->{biblionumber},
2035 'items' => $reserve->{itemnumber},
2040 =head2 GetReservesControlBranch
2042 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2044 Return the branchcode to be used to determine which reserves
2045 policy applies to a transaction.
2047 C<$item> is a hashref for an item. Only 'homebranch' is used.
2049 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2051 =cut
2053 sub GetReservesControlBranch {
2054 my ( $item, $borrower ) = @_;
2056 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2058 my $branchcode =
2059 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2060 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2061 : undef;
2063 return $branchcode;
2066 =head2 CalculatePriority
2068 my $p = CalculatePriority($biblionumber, $resdate);
2070 Calculate priority for a new reserve on biblionumber, placing it at
2071 the end of the line of all holds whose start date falls before
2072 the current system time and that are neither on the hold shelf
2073 or in transit.
2075 The reserve date parameter is optional; if it is supplied, the
2076 priority is based on the set of holds whose start date falls before
2077 the parameter value.
2079 After calculation of this priority, it is recommended to call
2080 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2081 AddReserves.
2083 =cut
2085 sub CalculatePriority {
2086 my ( $biblionumber, $resdate ) = @_;
2088 my $sql = q{
2089 SELECT COUNT(*) FROM reserves
2090 WHERE biblionumber = ?
2091 AND priority > 0
2092 AND (found IS NULL OR found = '')
2094 #skip found==W or found==T (waiting or transit holds)
2095 if( $resdate ) {
2096 $sql.= ' AND ( reservedate <= ? )';
2098 else {
2099 $sql.= ' AND ( reservedate < NOW() )';
2101 my $dbh = C4::Context->dbh();
2102 my @row = $dbh->selectrow_array(
2103 $sql,
2104 undef,
2105 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2108 return @row ? $row[0]+1 : 1;
2111 =head2 IsItemOnHoldAndFound
2113 my $bool = IsItemFoundHold( $itemnumber );
2115 Returns true if the item is currently on hold
2116 and that hold has a non-null found status ( W, T, etc. )
2118 =cut
2120 sub IsItemOnHoldAndFound {
2121 my ($itemnumber) = @_;
2123 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2125 my $found = $rs->count(
2127 itemnumber => $itemnumber,
2128 found => { '!=' => undef }
2132 return $found;
2135 =head2 GetMaxPatronHoldsForRecord
2137 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2139 For multiple holds on a given record for a given patron, the max
2140 number of record level holds that a patron can be placed is the highest
2141 value of the holds_per_record rule for each item if the record for that
2142 patron. This subroutine finds and returns the highest holds_per_record
2143 rule value for a given patron id and record id.
2145 =cut
2147 sub GetMaxPatronHoldsForRecord {
2148 my ( $borrowernumber, $biblionumber ) = @_;
2150 my $patron = Koha::Patrons->find($borrowernumber);
2151 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2153 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2155 my $categorycode = $patron->categorycode;
2156 my $branchcode;
2157 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2159 my $max = 0;
2160 foreach my $item (@items) {
2161 my $itemtype = $item->effective_itemtype();
2163 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2165 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2166 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2167 $max = $holds_per_record if $holds_per_record > $max;
2170 return $max;
2173 =head2 GetHoldRule
2175 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2177 Returns the matching hold related issuingrule fields for a given
2178 patron category, itemtype, and library.
2180 =cut
2182 sub GetHoldRule {
2183 my ( $categorycode, $itemtype, $branchcode ) = @_;
2185 my $dbh = C4::Context->dbh;
2187 my $sth = $dbh->prepare(
2189 SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record, holds_per_day
2190 FROM issuingrules
2191 WHERE (categorycode in (?,'*') )
2192 AND (itemtype IN (?,'*'))
2193 AND (branchcode IN (?,'*'))
2194 ORDER BY categorycode DESC,
2195 itemtype DESC,
2196 branchcode DESC
2200 $sth->execute( $categorycode, $itemtype, $branchcode );
2202 return $sth->fetchrow_hashref();
2205 =head1 AUTHOR
2207 Koha Development Team <http://koha-community.org/>
2209 =cut