Bug 24584: Rewrite optional/patron_atributes to YAML
[koha.git] / C4 / Reserves.pm
blob00d921d77eec9b1a837f041132cc81beb9c3ed72
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 Modern::Perl;
26 use C4::Accounts;
27 use C4::Biblio;
28 use C4::Circulation;
29 use C4::Context;
30 use C4::Items;
31 use C4::Letters;
32 use C4::Log;
33 use C4::Members::Messaging;
34 use C4::Members;
35 use Koha::Account::Lines;
36 use Koha::Biblios;
37 use Koha::Calendar;
38 use Koha::CirculationRules;
39 use Koha::Database;
40 use Koha::DateUtils;
41 use Koha::Hold;
42 use Koha::Holds;
43 use Koha::ItemTypes;
44 use Koha::Items;
45 use Koha::Libraries;
46 use Koha::Old::Hold;
47 use Koha::Patrons;
49 use Carp;
50 use Data::Dumper;
51 use List::MoreUtils qw( firstidx any );
53 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
55 =head1 NAME
57 C4::Reserves - Koha functions for dealing with reservation.
59 =head1 SYNOPSIS
61 use C4::Reserves;
63 =head1 DESCRIPTION
65 This modules provides somes functions to deal with reservations.
67 Reserves are stored in reserves table.
68 The following columns contains important values :
69 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
70 =0 : then the reserve is being dealed
71 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
72 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
73 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
74 F(inished) : the reserve has been completed, and is done
75 - itemnumber : empty : the reserve is still unaffected to an item
76 filled: the reserve is attached to an item
77 The complete workflow is :
78 ==== 1st use case ====
79 patron request a document, 1st available : P >0, F=NULL, I=NULL
80 a library having it run "transfertodo", and clic on the list
81 if there is no transfer to do, the reserve waiting
82 patron can pick it up P =0, F=W, I=filled
83 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
84 The pickup library receive the book, it check in P =0, F=W, I=filled
85 The patron borrow the book P =0, F=F, I=filled
87 ==== 2nd use case ====
88 patron requests a document, a given item,
89 If pickup is holding branch P =0, F=W, I=filled
90 If transfer needed, write in branchtransfer P =0, F=T, I=filled
91 The pickup library receive the book, it checks it in P =0, F=W, I=filled
92 The patron borrow the book P =0, F=F, I=filled
94 =head1 FUNCTIONS
96 =cut
98 BEGIN {
99 require Exporter;
100 @ISA = qw(Exporter);
101 @EXPORT = qw(
102 &AddReserve
104 &GetReserveStatus
106 &GetOtherReserves
108 &ModReserveFill
109 &ModReserveAffect
110 &ModReserve
111 &ModReserveStatus
112 &ModReserveCancelAll
113 &ModReserveMinusPriority
114 &MoveReserve
116 &CheckReserves
117 &CanBookBeReserved
118 &CanItemBeReserved
119 &CanReserveBeCanceledFromOpac
120 &CancelExpiredReserves
122 &AutoUnsuspendReserves
124 &IsAvailableForItemLevelRequest
126 &AlterPriority
127 &ToggleLowestPriority
129 &ReserveSlip
130 &ToggleSuspend
131 &SuspendAll
133 &GetReservesControlBranch
135 IsItemOnHoldAndFound
137 GetMaxPatronHoldsForRecord
139 @EXPORT_OK = qw( MergeHolds );
142 =head2 AddReserve
144 AddReserve(
146 branch => $branchcode,
147 borrowernumber => $borrowernumber,
148 biblionumber => $biblionumber,
149 priority => $priority,
150 reservation_date => $reservation_date,
151 expiration_date => $expiration_date,
152 notes => $notes,
153 title => $title,
154 itemnumber => $itemnumber,
155 found => $found,
156 itemtype => $itemtype,
160 Adds reserve and generates HOLDPLACED message.
162 The following tables are available witin the HOLDPLACED message:
164 branches
165 borrowers
166 biblio
167 biblioitems
168 items
169 reserves
171 =cut
173 sub AddReserve {
174 my ($params) = @_;
175 my $branch = $params->{branchcode};
176 my $borrowernumber = $params->{borrowernumber};
177 my $biblionumber = $params->{biblionumber};
178 my $priority = $params->{priority};
179 my $resdate = $params->{reservation_date};
180 my $expdate = $params->{expiration_date};
181 my $notes = $params->{notes};
182 my $title = $params->{title};
183 my $checkitem = $params->{itemnumber};
184 my $found = $params->{found};
185 my $itemtype = $params->{itemtype};
187 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
188 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
190 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
192 # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
193 # of the document, we force the value $priority and $found .
194 if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
195 my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
197 if (
198 # If item is already checked out, it cannot be set waiting
199 !$item->onloan
201 # The item can't be waiting if it needs a transfer
202 && $item->holdingbranch eq $branch
204 # Similarly, if in transit it can't be waiting
205 && !$item->get_transfer
207 # If we can't hold damaged items, and it is damaged, it can't be waiting
208 && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
210 # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
211 && !$item->current_holds->count )
213 $priority = 0;
214 $found = 'W';
218 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
220 # Make room in reserves for this before those of a later reserve date
221 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
224 my $waitingdate;
226 # If the reserv had the waiting status, we had the value of the resdate
227 if ( $found && $found eq 'W' ) {
228 $waitingdate = $resdate;
231 # Don't add itemtype limit if specific item is selected
232 $itemtype = undef if $checkitem;
234 # updates take place here
235 my $hold = Koha::Hold->new(
237 borrowernumber => $borrowernumber,
238 biblionumber => $biblionumber,
239 reservedate => $resdate,
240 branchcode => $branch,
241 priority => $priority,
242 reservenotes => $notes,
243 itemnumber => $checkitem,
244 found => $found,
245 waitingdate => $waitingdate,
246 expirationdate => $expdate,
247 itemtype => $itemtype,
248 item_level_hold => $checkitem ? 1 : 0,
250 )->store();
251 $hold->set_waiting() if $found && $found eq 'W';
253 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
254 if C4::Context->preference('HoldsLog');
256 my $reserve_id = $hold->id();
258 # add a reserve fee if needed
259 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
260 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
261 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
264 _FixPriority({ biblionumber => $biblionumber});
266 # Send e-mail to librarian if syspref is active
267 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
268 my $patron = Koha::Patrons->find( $borrowernumber );
269 my $library = $patron->library;
270 if ( my $letter = C4::Letters::GetPreparedLetter (
271 module => 'reserves',
272 letter_code => 'HOLDPLACED',
273 branchcode => $branch,
274 lang => $patron->lang,
275 tables => {
276 'branches' => $library->unblessed,
277 'borrowers' => $patron->unblessed,
278 'biblio' => $biblionumber,
279 'biblioitems' => $biblionumber,
280 'items' => $checkitem,
281 'reserves' => $hold->unblessed,
283 ) ) {
285 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
287 C4::Letters::EnqueueLetter(
288 { letter => $letter,
289 borrowernumber => $borrowernumber,
290 message_transport_type => 'email',
291 from_address => $admin_email_address,
292 to_address => $admin_email_address,
298 return $reserve_id;
301 =head2 CanBookBeReserved
303 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
304 if ($canReserve eq 'OK') { #We can reserve this Item! }
306 See CanItemBeReserved() for possible return values.
308 =cut
310 sub CanBookBeReserved{
311 my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
313 my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
314 #get items linked via host records
315 my @hostitems = get_hostitemnumbers_of($biblionumber);
316 if (@hostitems){
317 push (@itemnumbers, @hostitems);
320 my $canReserve = { status => '' };
321 foreach my $itemnumber (@itemnumbers) {
322 $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
323 return { status => 'OK' } if $canReserve->{status} eq 'OK';
325 return $canReserve;
328 =head2 CanItemBeReserved
330 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
331 if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
333 @RETURNS { status => OK }, if the Item can be reserved.
334 { status => ageRestricted }, if the Item is age restricted for this borrower.
335 { status => damaged }, if the Item is damaged.
336 { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
337 { status => branchNotInHoldGroup }, if borrower home library is not in hold group, and holds are only allowed from hold groups.
338 { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
339 { status => notReservable }, if holds on this item are not allowed
340 { status => libraryNotFound }, if given branchcode is not an existing library
341 { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location
342 { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
343 { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
345 =cut
347 sub CanItemBeReserved {
348 my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
350 my $dbh = C4::Context->dbh;
351 my $ruleitemtype; # itemtype of the matching issuing rule
352 my $allowedreserves = 0; # Total number of holds allowed across all records
353 my $holds_per_record = 1; # Total number of holds allowed for this one given record
354 my $holds_per_day; # Default to unlimited
356 # we retrieve borrowers and items informations #
357 # item->{itype} will come for biblioitems if necessery
358 my $item = Koha::Items->find($itemnumber);
359 my $biblio = $item->biblio;
360 my $patron = Koha::Patrons->find( $borrowernumber );
361 my $borrower = $patron->unblessed;
363 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
364 return { status =>'damaged' }
365 if ( $item->damaged
366 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
368 # Check for the age restriction
369 my ( $ageRestriction, $daysToAgeRestriction ) =
370 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
371 return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
373 # Check that the patron doesn't have an item level hold on this item already
374 return { status =>'itemAlreadyOnHold' }
375 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
377 my $controlbranch = C4::Context->preference('ReservesControlBranch');
379 my $querycount = q{
380 SELECT count(*) AS count
381 FROM reserves
382 LEFT JOIN items USING (itemnumber)
383 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
384 LEFT JOIN borrowers USING (borrowernumber)
385 WHERE borrowernumber = ?
388 my $branchcode = "";
389 my $branchfield = "reserves.branchcode";
391 if ( $controlbranch eq "ItemHomeLibrary" ) {
392 $branchfield = "items.homebranch";
393 $branchcode = $item->homebranch;
395 elsif ( $controlbranch eq "PatronLibrary" ) {
396 $branchfield = "borrowers.branchcode";
397 $branchcode = $borrower->{branchcode};
400 # we retrieve rights
401 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->effective_itemtype, $branchcode ) ) {
402 $ruleitemtype = $rights->{itemtype};
403 $allowedreserves = $rights->{reservesallowed} // $allowedreserves;
404 $holds_per_record = $rights->{holds_per_record} // $holds_per_record;
405 $holds_per_day = $rights->{holds_per_day};
407 else {
408 $ruleitemtype = undef;
411 my $holds = Koha::Holds->search(
413 borrowernumber => $borrowernumber,
414 biblionumber => $item->biblionumber,
417 if ( defined $holds_per_record && $holds_per_record ne ''
418 && $holds->count() >= $holds_per_record ) {
419 return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
422 my $today_holds = Koha::Holds->search({
423 borrowernumber => $borrowernumber,
424 reservedate => dt_from_string->date
427 if ( defined $holds_per_day && $holds_per_day ne ''
428 && $today_holds->count() >= $holds_per_day )
430 return { status => 'tooManyReservesToday', limit => $holds_per_day };
433 # we retrieve count
435 $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
437 # If using item-level itypes, fall back to the record
438 # level itemtype if the hold has no associated item
439 $querycount .=
440 C4::Context->preference('item-level_itypes')
441 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
442 : " AND biblioitems.itemtype = ?"
443 if defined $ruleitemtype;
445 my $sthcount = $dbh->prepare($querycount);
447 if ( defined $ruleitemtype ) {
448 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
450 else {
451 $sthcount->execute( $borrowernumber, $branchcode );
454 my $reservecount = "0";
455 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
456 $reservecount = $rowcount->{count};
459 # we check if it's ok or not
460 if ( defined $allowedreserves && $allowedreserves ne ''
461 && $reservecount >= $allowedreserves ) {
462 return { status => 'tooManyReserves', limit => $allowedreserves };
465 # Now we need to check hold limits by patron category
466 my $rule = Koha::CirculationRules->get_effective_rule(
468 categorycode => $borrower->{categorycode},
469 branchcode => $branchcode,
470 rule_name => 'max_holds',
473 if ( $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
474 my $total_holds_count = Koha::Holds->search(
476 borrowernumber => $borrower->{borrowernumber}
478 )->count();
480 return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
483 my $reserves_control_branch =
484 GetReservesControlBranch( $item->unblessed(), $borrower );
485 my $branchitemrule =
486 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype ); # FIXME Should not be item->effective_itemtype?
488 if ( $branchitemrule->{holdallowed} == 0 ) {
489 return { status => 'notReservable' };
492 if ( $branchitemrule->{holdallowed} == 1
493 && $borrower->{branchcode} ne $item->homebranch )
495 return { status => 'cannotReserveFromOtherBranches' };
498 my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
499 if ( $branchitemrule->{holdallowed} == 3) {
500 if($borrower->{branchcode} ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $borrower->{branchcode}} )) {
501 return { status => 'branchNotInHoldGroup' };
505 # If reservecount is ok, we check item branch if IndependentBranches is ON
506 # and canreservefromotherbranches is OFF
507 if ( C4::Context->preference('IndependentBranches')
508 and !C4::Context->preference('canreservefromotherbranches') )
510 if ( $item->homebranch ne $borrower->{branchcode} ) {
511 return { status => 'cannotReserveFromOtherBranches' };
515 if ($pickup_branchcode) {
516 my $destination = Koha::Libraries->find({
517 branchcode => $pickup_branchcode,
520 unless ($destination) {
521 return { status => 'libraryNotFound' };
523 unless ($destination->pickup_location) {
524 return { status => 'libraryNotPickupLocation' };
526 unless ($item->can_be_transferred({ to => $destination })) {
527 return { status => 'cannotBeTransferred' };
529 unless ($branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
530 return { status => 'pickupNotInHoldGroup' };
532 unless ($branchitemrule->{hold_fulfillment_policy} ne 'patrongroup' || Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
533 return { status => 'pickupNotInHoldGroup' };
537 return { status => 'OK' };
540 =head2 CanReserveBeCanceledFromOpac
542 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
544 returns 1 if reserve can be cancelled by user from OPAC.
545 First check if reserve belongs to user, next checks if reserve is not in
546 transfer or waiting status
548 =cut
550 sub CanReserveBeCanceledFromOpac {
551 my ($reserve_id, $borrowernumber) = @_;
553 return unless $reserve_id and $borrowernumber;
554 my $reserve = Koha::Holds->find($reserve_id);
556 return 0 unless $reserve->borrowernumber == $borrowernumber;
557 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
559 return 1;
563 =head2 GetOtherReserves
565 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
567 Check queued list of this document and check if this document must be transferred
569 =cut
571 sub GetOtherReserves {
572 my ($itemnumber) = @_;
573 my $messages;
574 my $nextreservinfo;
575 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
576 if ($checkreserves) {
577 my $item = Koha::Items->find($itemnumber);
578 if ( $item->holdingbranch ne $checkreserves->{'branchcode'} ) {
579 $messages->{'transfert'} = $checkreserves->{'branchcode'};
580 #minus priorities of others reservs
581 ModReserveMinusPriority(
582 $itemnumber,
583 $checkreserves->{'reserve_id'},
586 #launch the subroutine dotransfer
587 C4::Items::ModItemTransfer(
588 $itemnumber,
589 $item->holdingbranch,
590 $checkreserves->{'branchcode'}
595 #step 2b : case of a reservation on the same branch, set the waiting status
596 else {
597 $messages->{'waiting'} = 1;
598 ModReserveMinusPriority(
599 $itemnumber,
600 $checkreserves->{'reserve_id'},
602 ModReserveStatus($itemnumber,'W');
605 $nextreservinfo = $checkreserves->{'borrowernumber'};
608 return ( $messages, $nextreservinfo );
611 =head2 ChargeReserveFee
613 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
615 Charge the fee for a reserve (if $fee > 0)
617 =cut
619 sub ChargeReserveFee {
620 my ( $borrowernumber, $fee, $title ) = @_;
621 return if !$fee || $fee == 0; # the last test is needed to include 0.00
622 Koha::Account->new( { patron_id => $borrowernumber } )->add_debit(
624 amount => $fee,
625 description => $title,
626 note => undef,
627 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
628 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
629 interface => C4::Context->interface,
630 invoice_type => undef,
631 type => 'RESERVE',
632 item_id => undef
637 =head2 GetReserveFee
639 $fee = GetReserveFee( $borrowernumber, $biblionumber );
641 Calculate the fee for a reserve (if applicable).
643 =cut
645 sub GetReserveFee {
646 my ( $borrowernumber, $biblionumber ) = @_;
647 my $borquery = qq{
648 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
650 my $issue_qry = qq{
651 SELECT COUNT(*) FROM items
652 LEFT JOIN issues USING (itemnumber)
653 WHERE items.biblionumber=? AND issues.issue_id IS NULL
655 my $holds_qry = qq{
656 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
659 my $dbh = C4::Context->dbh;
660 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
661 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
662 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
663 # This is a reconstruction of the old code:
664 # Compare number of items with items issued, and optionally check holds
665 # If not all items are issued and there are no holds: charge no fee
666 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
667 my ( $notissued, $reserved );
668 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
669 ( $biblionumber ) );
670 if( $notissued ) {
671 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
672 ( $biblionumber, $borrowernumber ) );
673 $fee = 0 if $reserved == 0;
676 return $fee;
679 =head2 GetReserveStatus
681 $reservestatus = GetReserveStatus($itemnumber);
683 Takes an itemnumber and returns the status of the reserve placed on it.
684 If several reserves exist, the reserve with the lower priority is given.
686 =cut
688 ## FIXME: I don't think this does what it thinks it does.
689 ## It only ever checks the first reserve result, even though
690 ## multiple reserves for that bib can have the itemnumber set
691 ## the sub is only used once in the codebase.
692 sub GetReserveStatus {
693 my ($itemnumber) = @_;
695 my $dbh = C4::Context->dbh;
697 my ($sth, $found, $priority);
698 if ( $itemnumber ) {
699 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
700 $sth->execute($itemnumber);
701 ($found, $priority) = $sth->fetchrow_array;
704 if(defined $found) {
705 return 'Waiting' if $found eq 'W' and $priority == 0;
706 return 'Finished' if $found eq 'F';
709 return 'Reserved' if defined $priority && $priority > 0;
711 return ''; # empty string here will remove need for checking undef, or less log lines
714 =head2 CheckReserves
716 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber);
717 ($status, $matched_reserve, $possible_reserves) = &CheckReserves(undef, $barcode);
718 ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
720 Find a book in the reserves.
722 C<$itemnumber> is the book's item number.
723 C<$lookahead> is the number of days to look in advance for future reserves.
725 As I understand it, C<&CheckReserves> looks for the given item in the
726 reserves. If it is found, that's a match, and C<$status> is set to
727 C<Waiting>.
729 Otherwise, it finds the most important item in the reserves with the
730 same biblio number as this book (I'm not clear on this) and returns it
731 with C<$status> set to C<Reserved>.
733 C<&CheckReserves> returns a two-element list:
735 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
737 C<$reserve> is the reserve item that matched. It is a
738 reference-to-hash whose keys are mostly the fields of the reserves
739 table in the Koha database.
741 =cut
743 sub CheckReserves {
744 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
745 my $dbh = C4::Context->dbh;
746 my $sth;
747 my $select;
748 if (C4::Context->preference('item-level_itypes')){
749 $select = "
750 SELECT items.biblionumber,
751 items.biblioitemnumber,
752 itemtypes.notforloan,
753 items.notforloan AS itemnotforloan,
754 items.itemnumber,
755 items.damaged,
756 items.homebranch,
757 items.holdingbranch
758 FROM items
759 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
760 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
763 else {
764 $select = "
765 SELECT items.biblionumber,
766 items.biblioitemnumber,
767 itemtypes.notforloan,
768 items.notforloan AS itemnotforloan,
769 items.itemnumber,
770 items.damaged,
771 items.homebranch,
772 items.holdingbranch
773 FROM items
774 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
775 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
779 if ($item) {
780 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
781 $sth->execute($item);
783 else {
784 $sth = $dbh->prepare("$select WHERE barcode = ?");
785 $sth->execute($barcode);
787 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
788 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
789 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
791 return unless $itemnumber; # bail if we got nothing.
792 # if item is not for loan it cannot be reserved either.....
793 # except where items.notforloan < 0 : This indicates the item is holdable.
794 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
796 # Find this item in the reserves
797 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
799 # $priority and $highest are used to find the most important item
800 # in the list returned by &_Findgroupreserve. (The lower $priority,
801 # the more important the item.)
802 # $highest is the most important item we've seen so far.
803 my $highest;
804 if (scalar @reserves) {
805 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
806 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
807 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
809 my $priority = 10000000;
810 foreach my $res (@reserves) {
811 if ( $res->{'itemnumber'} && $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
812 if ($res->{'found'} eq 'W') {
813 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
814 } else {
815 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
817 } else {
818 my $patron;
819 my $item;
820 my $local_hold_match;
822 if ($LocalHoldsPriority) {
823 $patron = Koha::Patrons->find( $res->{borrowernumber} );
824 $item = Koha::Items->find($itemnumber);
826 my $local_holds_priority_item_branchcode =
827 $item->$LocalHoldsPriorityItemControl;
828 my $local_holds_priority_patron_branchcode =
829 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
830 ? $res->{branchcode}
831 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
832 ? $patron->branchcode
833 : undef;
834 $local_hold_match =
835 $local_holds_priority_item_branchcode eq
836 $local_holds_priority_patron_branchcode;
839 # See if this item is more important than what we've got so far
840 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
841 $item ||= Koha::Items->find($itemnumber);
842 next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
843 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
844 my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
845 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
846 next if ($branchitemrule->{'holdallowed'} == 0);
847 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
848 my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
849 next if (($branchitemrule->{'holdallowed'} == 3) && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
850 my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
851 next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
852 next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
853 next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
854 next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
855 $priority = $res->{'priority'};
856 $highest = $res;
857 last if $local_hold_match;
863 # If we get this far, then no exact match was found.
864 # We return the most important (i.e. next) reservation.
865 if ($highest) {
866 $highest->{'itemnumber'} = $item;
867 return ( "Reserved", $highest, \@reserves );
870 return ( '' );
873 =head2 CancelExpiredReserves
875 CancelExpiredReserves();
877 Cancels all reserves with an expiration date from before today.
879 =cut
881 sub CancelExpiredReserves {
882 my $today = dt_from_string();
883 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
884 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
886 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
887 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
888 $params->{found} = [ { '!=', 'W' }, undef ] unless $expireWaiting;
890 # FIXME To move to Koha::Holds->search_expired (?)
891 my $holds = Koha::Holds->search( $params );
893 while ( my $hold = $holds->next ) {
894 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
896 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
898 my $cancel_params = {};
899 if ( $hold->found eq 'W' ) {
900 $cancel_params->{charge_cancel_fee} = 1;
902 $hold->cancel( $cancel_params );
906 =head2 AutoUnsuspendReserves
908 AutoUnsuspendReserves();
910 Unsuspends all suspended reserves with a suspend_until date from before today.
912 =cut
914 sub AutoUnsuspendReserves {
915 my $today = dt_from_string();
917 my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
919 map { $_->resume() } @holds;
922 =head2 ModReserve
924 ModReserve({ rank => $rank,
925 reserve_id => $reserve_id,
926 branchcode => $branchcode
927 [, itemnumber => $itemnumber ]
928 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
931 Change a hold request's priority or cancel it.
933 C<$rank> specifies the effect of the change. If C<$rank>
934 is 'W' or 'n', nothing happens. This corresponds to leaving a
935 request alone when changing its priority in the holds queue
936 for a bib.
938 If C<$rank> is 'del', the hold request is cancelled.
940 If C<$rank> is an integer greater than zero, the priority of
941 the request is set to that value. Since priority != 0 means
942 that the item is not waiting on the hold shelf, setting the
943 priority to a non-zero value also sets the request's found
944 status and waiting date to NULL.
946 The optional C<$itemnumber> parameter is used only when
947 C<$rank> is a non-zero integer; if supplied, the itemnumber
948 of the hold request is set accordingly; if omitted, the itemnumber
949 is cleared.
951 B<FIXME:> Note that the forgoing can have the effect of causing
952 item-level hold requests to turn into title-level requests. This
953 will be fixed once reserves has separate columns for requested
954 itemnumber and supplying itemnumber.
956 =cut
958 sub ModReserve {
959 my ( $params ) = @_;
961 my $rank = $params->{'rank'};
962 my $reserve_id = $params->{'reserve_id'};
963 my $branchcode = $params->{'branchcode'};
964 my $itemnumber = $params->{'itemnumber'};
965 my $suspend_until = $params->{'suspend_until'};
966 my $borrowernumber = $params->{'borrowernumber'};
967 my $biblionumber = $params->{'biblionumber'};
969 return if $rank eq "W";
970 return if $rank eq "n";
972 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
974 my $hold;
975 unless ( $reserve_id ) {
976 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
977 return unless $holds->count; # FIXME Should raise an exception
978 $hold = $holds->next;
979 $reserve_id = $hold->reserve_id;
982 $hold ||= Koha::Holds->find($reserve_id);
984 if ( $rank eq "del" ) {
985 $hold->cancel;
987 elsif ($rank =~ /^\d+/ and $rank > 0) {
988 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
989 if C4::Context->preference('HoldsLog');
991 my $properties = {
992 priority => $rank,
993 branchcode => $branchcode,
994 itemnumber => $itemnumber,
995 found => undef,
996 waitingdate => undef
998 if (exists $params->{reservedate}) {
999 $properties->{reservedate} = $params->{reservedate} || undef;
1001 if (exists $params->{expirationdate}) {
1002 $properties->{expirationdate} = $params->{expirationdate} || undef;
1005 $hold->set($properties)->store();
1007 if ( defined( $suspend_until ) ) {
1008 if ( $suspend_until ) {
1009 $suspend_until = eval { dt_from_string( $suspend_until ) };
1010 $hold->suspend_hold( $suspend_until );
1011 } else {
1012 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1013 # If the hold is not suspended, this does nothing.
1014 $hold->set( { suspend_until => undef } )->store();
1018 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1022 =head2 ModReserveFill
1024 &ModReserveFill($reserve);
1026 Fill a reserve. If I understand this correctly, this means that the
1027 reserved book has been found and given to the patron who reserved it.
1029 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1030 whose keys are fields from the reserves table in the Koha database.
1032 =cut
1034 sub ModReserveFill {
1035 my ($res) = @_;
1036 my $reserve_id = $res->{'reserve_id'};
1038 my $hold = Koha::Holds->find($reserve_id);
1039 # get the priority on this record....
1040 my $priority = $hold->priority;
1042 # update the hold statuses, no need to store it though, we will be deleting it anyway
1043 $hold->set(
1045 found => 'F',
1046 priority => 0,
1050 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
1051 Koha::Old::Hold->new( $hold->unblessed() )->store();
1053 $hold->delete();
1055 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1056 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1057 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1060 # now fix the priority on the others (if the priority wasn't
1061 # already sorted!)....
1062 unless ( $priority == 0 ) {
1063 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1067 =head2 ModReserveStatus
1069 &ModReserveStatus($itemnumber, $newstatus);
1071 Update the reserve status for the active (priority=0) reserve.
1073 $itemnumber is the itemnumber the reserve is on
1075 $newstatus is the new status.
1077 =cut
1079 sub ModReserveStatus {
1081 #first : check if we have a reservation for this item .
1082 my ($itemnumber, $newstatus) = @_;
1083 my $dbh = C4::Context->dbh;
1085 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1086 my $sth_set = $dbh->prepare($query);
1087 $sth_set->execute( $newstatus, $itemnumber );
1089 my $item = Koha::Items->find($itemnumber);
1090 if ( $item->location && $item->location eq 'CART'
1091 && ( !$item->permanent_location || $item->permanent_location ne 'CART' )
1092 && $newstatus ) {
1093 CartToShelf( $itemnumber );
1097 =head2 ModReserveAffect
1099 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1101 This function affect an item and a status for a given reserve, either fetched directly
1102 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1103 is given, only first reserve returned is affected, which is ok for anything but
1104 multi-item holds.
1106 if $transferToDo is not set, then the status is set to "Waiting" as well.
1107 otherwise, a transfer is on the way, and the end of the transfer will
1108 take care of the waiting status
1110 =cut
1112 sub ModReserveAffect {
1113 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1114 my $dbh = C4::Context->dbh;
1116 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1117 # attached to $itemnumber
1118 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1119 $sth->execute($itemnumber);
1120 my ($biblionumber) = $sth->fetchrow;
1122 # get request - need to find out if item is already
1123 # waiting in order to not send duplicate hold filled notifications
1125 my $hold;
1126 # Find hold by id if we have it
1127 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1128 # Find item level hold for this item if there is one
1129 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1130 # Find record level hold if there is no item level hold
1131 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1133 return unless $hold;
1135 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1137 $hold->itemnumber($itemnumber);
1138 $hold->set_waiting($transferToDo);
1140 _koha_notify_reserve( $hold->reserve_id )
1141 if ( !$transferToDo && !$already_on_shelf );
1143 _FixPriority( { biblionumber => $biblionumber } );
1144 my $item = Koha::Items->find($itemnumber);
1145 if ( $item->location && $item->location eq 'CART'
1146 && ( !$item->permanent_location || $item->permanent_location ne 'CART' ) ) {
1147 CartToShelf( $itemnumber );
1150 return;
1153 =head2 ModReserveCancelAll
1155 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1157 function to cancel reserv,check other reserves, and transfer document if it's necessary
1159 =cut
1161 sub ModReserveCancelAll {
1162 my $messages;
1163 my $nextreservinfo;
1164 my ( $itemnumber, $borrowernumber ) = @_;
1166 #step 1 : cancel the reservation
1167 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1168 return unless $holds->count;
1169 $holds->next->cancel;
1171 #step 2 launch the subroutine of the others reserves
1172 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1174 return ( $messages, $nextreservinfo );
1177 =head2 ModReserveMinusPriority
1179 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1181 Reduce the values of queued list
1183 =cut
1185 sub ModReserveMinusPriority {
1186 my ( $itemnumber, $reserve_id ) = @_;
1188 #first step update the value of the first person on reserv
1189 my $dbh = C4::Context->dbh;
1190 my $query = "
1191 UPDATE reserves
1192 SET priority = 0 , itemnumber = ?
1193 WHERE reserve_id = ?
1195 my $sth_upd = $dbh->prepare($query);
1196 $sth_upd->execute( $itemnumber, $reserve_id );
1197 # second step update all others reserves
1198 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1201 =head2 IsAvailableForItemLevelRequest
1203 my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1205 Checks whether a given item record is available for an
1206 item-level hold request. An item is available if
1208 * it is not lost AND
1209 * it is not damaged AND
1210 * it is not withdrawn AND
1211 * a waiting or in transit reserve is placed on
1212 * does not have a not for loan value > 0
1214 Need to check the issuingrules onshelfholds column,
1215 if this is set items on the shelf can be placed on hold
1217 Note that IsAvailableForItemLevelRequest() does not
1218 check if the staff operator is authorized to place
1219 a request on the item - in particular,
1220 this routine does not check IndependentBranches
1221 and canreservefromotherbranches.
1223 =cut
1225 sub IsAvailableForItemLevelRequest {
1226 my ( $item, $patron, $pickup_branchcode ) = @_;
1228 my $dbh = C4::Context->dbh;
1229 # must check the notforloan setting of the itemtype
1230 # FIXME - a lot of places in the code do this
1231 # or something similar - need to be
1232 # consolidated
1233 my $itemtype = $item->effective_itemtype;
1234 my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1236 return 0 if
1237 $notforloan_per_itemtype ||
1238 $item->itemlost ||
1239 $item->notforloan > 0 ||
1240 $item->withdrawn ||
1241 ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1243 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1245 if ($pickup_branchcode) {
1246 my $destination = Koha::Libraries->find($pickup_branchcode);
1247 return 0 unless $destination;
1248 return 0 unless $destination->pickup_location;
1249 return 0 unless $item->can_be_transferred( { to => $destination } );
1250 my $reserves_control_branch =
1251 GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
1252 my $branchitemrule =
1253 C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
1254 my $home_library = Koka::Libraries->find( {branchcode => $item->homebranch} );
1255 return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
1258 if ( $on_shelf_holds == 1 ) {
1259 return 1;
1260 } elsif ( $on_shelf_holds == 2 ) {
1261 my @items =
1262 Koha::Items->search( { biblionumber => $item->biblionumber } );
1264 my $any_available = 0;
1266 foreach my $i (@items) {
1267 my $reserves_control_branch = GetReservesControlBranch( $i->unblessed(), $patron->unblessed );
1268 my $branchitemrule = C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1269 my $item_library = Koha::Libraries->find( {branchcode => $i->homebranch} );
1272 $any_available = 1
1273 unless $i->itemlost
1274 || $i->notforloan > 0
1275 || $i->withdrawn
1276 || $i->onloan
1277 || IsItemOnHoldAndFound( $i->id )
1278 || ( $i->damaged
1279 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1280 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1281 || $branchitemrule->{holdallowed} == 1 && $patron->branchcode ne $i->homebranch
1282 || $branchitemrule->{holdallowed} == 3 && !$item_library->validate_hold_sibling( {branchcode => $patron->branchcode} );
1285 return $any_available ? 0 : 1;
1286 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1287 return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1291 sub _get_itype {
1292 my $item = shift;
1294 my $itype;
1295 if (C4::Context->preference('item-level_itypes')) {
1296 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1297 # When GetItem is fixed, we can remove this
1298 $itype = $item->{itype};
1300 else {
1301 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1302 # So if we already have a biblioitems join when calling this function,
1303 # we don't need to access the database again
1304 $itype = $item->{itemtype};
1306 unless ($itype) {
1307 my $dbh = C4::Context->dbh;
1308 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1309 my $sth = $dbh->prepare($query);
1310 $sth->execute($item->{biblioitemnumber});
1311 if (my $data = $sth->fetchrow_hashref()){
1312 $itype = $data->{itemtype};
1315 return $itype;
1318 =head2 AlterPriority
1320 AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1322 This function changes a reserve's priority up, down, to the top, or to the bottom.
1323 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1325 =cut
1327 sub AlterPriority {
1328 my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1330 my $hold = Koha::Holds->find( $reserve_id );
1331 return unless $hold;
1333 if ( $hold->cancellationdate ) {
1334 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1335 return;
1338 if ( $where eq 'up' ) {
1339 return unless $prev_priority;
1340 _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1341 } elsif ( $where eq 'down' ) {
1342 return unless $next_priority;
1343 _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1344 } elsif ( $where eq 'top' ) {
1345 _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1346 } elsif ( $where eq 'bottom' ) {
1347 _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1350 # FIXME Should return the new priority
1353 =head2 ToggleLowestPriority
1355 ToggleLowestPriority( $borrowernumber, $biblionumber );
1357 This function sets the lowestPriority field to true if is false, and false if it is true.
1359 =cut
1361 sub ToggleLowestPriority {
1362 my ( $reserve_id ) = @_;
1364 my $dbh = C4::Context->dbh;
1366 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1367 $sth->execute( $reserve_id );
1369 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1372 =head2 ToggleSuspend
1374 ToggleSuspend( $reserve_id );
1376 This function sets the suspend field to true if is false, and false if it is true.
1377 If the reserve is currently suspended with a suspend_until date, that date will
1378 be cleared when it is unsuspended.
1380 =cut
1382 sub ToggleSuspend {
1383 my ( $reserve_id, $suspend_until ) = @_;
1385 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1387 my $hold = Koha::Holds->find( $reserve_id );
1389 if ( $hold->is_suspended ) {
1390 $hold->resume()
1391 } else {
1392 $hold->suspend_hold( $suspend_until );
1396 =head2 SuspendAll
1398 SuspendAll(
1399 borrowernumber => $borrowernumber,
1400 [ biblionumber => $biblionumber, ]
1401 [ suspend_until => $suspend_until, ]
1402 [ suspend => $suspend ]
1405 This function accepts a set of hash keys as its parameters.
1406 It requires either borrowernumber or biblionumber, or both.
1408 suspend_until is wholly optional.
1410 =cut
1412 sub SuspendAll {
1413 my %params = @_;
1415 my $borrowernumber = $params{'borrowernumber'} || undef;
1416 my $biblionumber = $params{'biblionumber'} || undef;
1417 my $suspend_until = $params{'suspend_until'} || undef;
1418 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1420 $suspend_until = eval { dt_from_string($suspend_until) }
1421 if ( defined($suspend_until) );
1423 return unless ( $borrowernumber || $biblionumber );
1425 my $params;
1426 $params->{found} = undef;
1427 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1428 $params->{biblionumber} = $biblionumber if $biblionumber;
1430 my @holds = Koha::Holds->search($params);
1432 if ($suspend) {
1433 map { $_->suspend_hold($suspend_until) } @holds;
1435 else {
1436 map { $_->resume() } @holds;
1441 =head2 _FixPriority
1443 _FixPriority({
1444 reserve_id => $reserve_id,
1445 [rank => $rank,]
1446 [ignoreSetLowestRank => $ignoreSetLowestRank]
1451 _FixPriority({ biblionumber => $biblionumber});
1453 This routine adjusts the priority of a hold request and holds
1454 on the same bib.
1456 In the first form, where a reserve_id is passed, the priority of the
1457 hold is set to supplied rank, and other holds for that bib are adjusted
1458 accordingly. If the rank is "del", the hold is cancelled. If no rank
1459 is supplied, all of the holds on that bib have their priority adjusted
1460 as if the second form had been used.
1462 In the second form, where a biblionumber is passed, the holds on that
1463 bib (that are not captured) are sorted in order of increasing priority,
1464 then have reserves.priority set so that the first non-captured hold
1465 has its priority set to 1, the second non-captured hold has its priority
1466 set to 2, and so forth.
1468 In both cases, holds that have the lowestPriority flag on are have their
1469 priority adjusted to ensure that they remain at the end of the line.
1471 Note that the ignoreSetLowestRank parameter is meant to be used only
1472 when _FixPriority calls itself.
1474 =cut
1476 sub _FixPriority {
1477 my ( $params ) = @_;
1478 my $reserve_id = $params->{reserve_id};
1479 my $rank = $params->{rank} // '';
1480 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1481 my $biblionumber = $params->{biblionumber};
1483 my $dbh = C4::Context->dbh;
1485 my $hold;
1486 if ( $reserve_id ) {
1487 $hold = Koha::Holds->find( $reserve_id );
1488 return unless $hold;
1491 unless ( $biblionumber ) { # FIXME This is a very weird API
1492 $biblionumber = $hold->biblionumber;
1495 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1496 $hold->cancel;
1498 elsif ( $reserve_id && ( $rank eq "W" || $rank eq "0" ) ) {
1500 # make sure priority for waiting or in-transit items is 0
1501 my $query = "
1502 UPDATE reserves
1503 SET priority = 0
1504 WHERE reserve_id = ?
1505 AND found IN ('W', 'T')
1507 my $sth = $dbh->prepare($query);
1508 $sth->execute( $reserve_id );
1510 my @priority;
1512 # get whats left
1513 my $query = "
1514 SELECT reserve_id, borrowernumber, reservedate
1515 FROM reserves
1516 WHERE biblionumber = ?
1517 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1518 ORDER BY priority ASC
1520 my $sth = $dbh->prepare($query);
1521 $sth->execute( $biblionumber );
1522 while ( my $line = $sth->fetchrow_hashref ) {
1523 push( @priority, $line );
1526 # FIXME This whole sub must be rewritten, especially to highlight what is done when reserve_id is not given
1527 # To find the matching index
1528 my $i;
1529 my $key = -1; # to allow for 0 to be a valid result
1530 for ( $i = 0 ; $i < @priority ; $i++ ) {
1531 if ( $reserve_id && $reserve_id == $priority[$i]->{'reserve_id'} ) {
1532 $key = $i; # save the index
1533 last;
1537 # if index exists in array then move it to new position
1538 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1539 my $new_rank = $rank -
1540 1; # $new_rank is what you want the new index to be in the array
1541 my $moving_item = splice( @priority, $key, 1 );
1542 splice( @priority, $new_rank, 0, $moving_item );
1545 # now fix the priority on those that are left....
1546 $query = "
1547 UPDATE reserves
1548 SET priority = ?
1549 WHERE reserve_id = ?
1551 $sth = $dbh->prepare($query);
1552 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1553 $sth->execute(
1554 $j + 1,
1555 $priority[$j]->{'reserve_id'}
1559 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1560 $sth->execute();
1562 unless ( $ignoreSetLowestRank ) {
1563 while ( my $res = $sth->fetchrow_hashref() ) {
1564 _FixPriority({
1565 reserve_id => $res->{'reserve_id'},
1566 rank => '999999',
1567 ignoreSetLowestRank => 1
1573 =head2 _Findgroupreserve
1575 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1577 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1578 first match found. If neither, then we look for non-holds-queue based holds.
1579 Lookahead is the number of days to look in advance.
1581 C<&_Findgroupreserve> returns :
1582 C<@results> is an array of references-to-hash whose keys are mostly
1583 fields from the reserves table of the Koha database, plus
1584 C<biblioitemnumber>.
1586 This routine with either return:
1587 1 - Item specific holds from the holds queue
1588 2 - Title level holds from the holds queue
1589 3 - All holds for this biblionumber
1591 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1593 =cut
1595 sub _Findgroupreserve {
1596 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1597 my $dbh = C4::Context->dbh;
1599 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1600 # check for exact targeted match
1601 my $item_level_target_query = qq{
1602 SELECT reserves.biblionumber AS biblionumber,
1603 reserves.borrowernumber AS borrowernumber,
1604 reserves.reservedate AS reservedate,
1605 reserves.branchcode AS branchcode,
1606 reserves.cancellationdate AS cancellationdate,
1607 reserves.found AS found,
1608 reserves.reservenotes AS reservenotes,
1609 reserves.priority AS priority,
1610 reserves.timestamp AS timestamp,
1611 biblioitems.biblioitemnumber AS biblioitemnumber,
1612 reserves.itemnumber AS itemnumber,
1613 reserves.reserve_id AS reserve_id,
1614 reserves.itemtype AS itemtype
1615 FROM reserves
1616 JOIN biblioitems USING (biblionumber)
1617 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1618 WHERE found IS NULL
1619 AND priority > 0
1620 AND item_level_request = 1
1621 AND itemnumber = ?
1622 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1623 AND suspend = 0
1624 ORDER BY priority
1626 my $sth = $dbh->prepare($item_level_target_query);
1627 $sth->execute($itemnumber, $lookahead||0);
1628 my @results;
1629 if ( my $data = $sth->fetchrow_hashref ) {
1630 push( @results, $data )
1631 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1633 return @results if @results;
1635 # check for title-level targeted match
1636 my $title_level_target_query = qq{
1637 SELECT reserves.biblionumber AS biblionumber,
1638 reserves.borrowernumber AS borrowernumber,
1639 reserves.reservedate AS reservedate,
1640 reserves.branchcode AS branchcode,
1641 reserves.cancellationdate AS cancellationdate,
1642 reserves.found AS found,
1643 reserves.reservenotes AS reservenotes,
1644 reserves.priority AS priority,
1645 reserves.timestamp AS timestamp,
1646 biblioitems.biblioitemnumber AS biblioitemnumber,
1647 reserves.itemnumber AS itemnumber,
1648 reserves.reserve_id AS reserve_id,
1649 reserves.itemtype AS itemtype
1650 FROM reserves
1651 JOIN biblioitems USING (biblionumber)
1652 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1653 WHERE found IS NULL
1654 AND priority > 0
1655 AND item_level_request = 0
1656 AND hold_fill_targets.itemnumber = ?
1657 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1658 AND suspend = 0
1659 ORDER BY priority
1661 $sth = $dbh->prepare($title_level_target_query);
1662 $sth->execute($itemnumber, $lookahead||0);
1663 @results = ();
1664 if ( my $data = $sth->fetchrow_hashref ) {
1665 push( @results, $data )
1666 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1668 return @results if @results;
1670 my $query = qq{
1671 SELECT reserves.biblionumber AS biblionumber,
1672 reserves.borrowernumber AS borrowernumber,
1673 reserves.reservedate AS reservedate,
1674 reserves.waitingdate AS waitingdate,
1675 reserves.branchcode AS branchcode,
1676 reserves.cancellationdate AS cancellationdate,
1677 reserves.found AS found,
1678 reserves.reservenotes AS reservenotes,
1679 reserves.priority AS priority,
1680 reserves.timestamp AS timestamp,
1681 reserves.itemnumber AS itemnumber,
1682 reserves.reserve_id AS reserve_id,
1683 reserves.itemtype AS itemtype
1684 FROM reserves
1685 WHERE reserves.biblionumber = ?
1686 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1687 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1688 AND suspend = 0
1689 ORDER BY priority
1691 $sth = $dbh->prepare($query);
1692 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1693 @results = ();
1694 while ( my $data = $sth->fetchrow_hashref ) {
1695 push( @results, $data )
1696 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1698 return @results;
1701 =head2 _koha_notify_reserve
1703 _koha_notify_reserve( $hold->reserve_id );
1705 Sends a notification to the patron that their hold has been filled (through
1706 ModReserveAffect, _not_ ModReserveFill)
1708 The letter code for this notice may be found using the following query:
1710 select distinct letter_code
1711 from message_transports
1712 inner join message_attributes using (message_attribute_id)
1713 where message_name = 'Hold_Filled'
1715 This will probably sipmly be 'HOLD', but because it is defined in the database,
1716 it is subject to addition or change.
1718 The following tables are availalbe witin the notice:
1720 branches
1721 borrowers
1722 biblio
1723 biblioitems
1724 reserves
1725 items
1727 =cut
1729 sub _koha_notify_reserve {
1730 my $reserve_id = shift;
1731 my $hold = Koha::Holds->find($reserve_id);
1732 my $borrowernumber = $hold->borrowernumber;
1734 my $patron = Koha::Patrons->find( $borrowernumber );
1736 # Try to get the borrower's email address
1737 my $to_address = $patron->notice_email_address;
1739 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1740 borrowernumber => $borrowernumber,
1741 message_name => 'Hold_Filled'
1742 } );
1744 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1746 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1748 my %letter_params = (
1749 module => 'reserves',
1750 branchcode => $hold->branchcode,
1751 lang => $patron->lang,
1752 tables => {
1753 'branches' => $library,
1754 'borrowers' => $patron->unblessed,
1755 'biblio' => $hold->biblionumber,
1756 'biblioitems' => $hold->biblionumber,
1757 'reserves' => $hold->unblessed,
1758 'items' => $hold->itemnumber,
1762 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.
1763 my $send_notification = sub {
1764 my ( $mtt, $letter_code ) = (@_);
1765 return unless defined $letter_code;
1766 $letter_params{letter_code} = $letter_code;
1767 $letter_params{message_transport_type} = $mtt;
1768 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1769 unless ($letter) {
1770 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1771 return;
1774 C4::Letters::EnqueueLetter( {
1775 letter => $letter,
1776 borrowernumber => $borrowernumber,
1777 from_address => $admin_email_address,
1778 message_transport_type => $mtt,
1779 } );
1782 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1783 next if (
1784 ( $mtt eq 'email' and not $to_address ) # No email address
1785 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1786 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1789 &$send_notification($mtt, $letter_code);
1790 $notification_sent++;
1792 #Making sure that a print notification is sent if no other transport types can be utilized.
1793 if (! $notification_sent) {
1794 &$send_notification('print', 'HOLD');
1799 =head2 _ShiftPriorityByDateAndPriority
1801 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1803 This increments the priority of all reserves after the one
1804 with either the lowest date after C<$reservedate>
1805 or the lowest priority after C<$priority>.
1807 It effectively makes room for a new reserve to be inserted with a certain
1808 priority, which is returned.
1810 This is most useful when the reservedate can be set by the user. It allows
1811 the new reserve to be placed before other reserves that have a later
1812 reservedate. Since priority also is set by the form in reserves/request.pl
1813 the sub accounts for that too.
1815 =cut
1817 sub _ShiftPriorityByDateAndPriority {
1818 my ( $biblio, $resdate, $new_priority ) = @_;
1820 my $dbh = C4::Context->dbh;
1821 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1822 my $sth = $dbh->prepare( $query );
1823 $sth->execute( $biblio, $resdate, $new_priority );
1824 my $min_priority = $sth->fetchrow;
1825 # if no such matches are found, $new_priority remains as original value
1826 $new_priority = $min_priority if ( $min_priority );
1828 # Shift the priority up by one; works in conjunction with the next SQL statement
1829 $query = "UPDATE reserves
1830 SET priority = priority+1
1831 WHERE biblionumber = ?
1832 AND borrowernumber = ?
1833 AND reservedate = ?
1834 AND found IS NULL";
1835 my $sth_update = $dbh->prepare( $query );
1837 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1838 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1839 $sth = $dbh->prepare( $query );
1840 $sth->execute( $new_priority, $biblio );
1841 while ( my $row = $sth->fetchrow_hashref ) {
1842 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1845 return $new_priority; # so the caller knows what priority they wind up receiving
1848 =head2 MoveReserve
1850 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1852 Use when checking out an item to handle reserves
1853 If $cancelreserve boolean is set to true, it will remove existing reserve
1855 =cut
1857 sub MoveReserve {
1858 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1860 $cancelreserve //= 0;
1862 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1863 my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1864 return unless $res;
1866 my $biblionumber = $res->{biblionumber};
1868 if ($res->{borrowernumber} == $borrowernumber) {
1869 ModReserveFill($res);
1871 else {
1872 # warn "Reserved";
1873 # The item is reserved by someone else.
1874 # Find this item in the reserves
1876 my $borr_res = Koha::Holds->search({
1877 borrowernumber => $borrowernumber,
1878 biblionumber => $biblionumber,
1880 order_by => 'priority'
1881 })->next();
1883 if ( $borr_res ) {
1884 # The item is reserved by the current patron
1885 ModReserveFill($borr_res->unblessed);
1888 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1889 RevertWaitingStatus({ itemnumber => $itemnumber });
1891 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1892 my $hold = Koha::Holds->find( $res->{reserve_id} );
1893 $hold->cancel;
1898 =head2 MergeHolds
1900 MergeHolds($dbh,$to_biblio, $from_biblio);
1902 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1904 =cut
1906 sub MergeHolds {
1907 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1908 my $sth = $dbh->prepare(
1909 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1911 $sth->execute($from_biblio);
1912 if ( my $data = $sth->fetchrow_hashref() ) {
1914 # holds exist on old record, if not we don't need to do anything
1915 $sth = $dbh->prepare(
1916 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1917 $sth->execute( $to_biblio, $from_biblio );
1919 # Reorder by date
1920 # don't reorder those already waiting
1922 $sth = $dbh->prepare(
1923 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1925 my $upd_sth = $dbh->prepare(
1926 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1927 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1929 $sth->execute( $to_biblio, 'W', 'T' );
1930 my $priority = 1;
1931 while ( my $reserve = $sth->fetchrow_hashref() ) {
1932 $upd_sth->execute(
1933 $priority, $to_biblio,
1934 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1935 $reserve->{'itemnumber'}
1937 $priority++;
1942 =head2 RevertWaitingStatus
1944 RevertWaitingStatus({ itemnumber => $itemnumber });
1946 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1948 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1949 item level hold, even if it was only a bibliolevel hold to
1950 begin with. This is because we can no longer know if a hold
1951 was item-level or bib-level after a hold has been set to
1952 waiting status.
1954 =cut
1956 sub RevertWaitingStatus {
1957 my ( $params ) = @_;
1958 my $itemnumber = $params->{'itemnumber'};
1960 return unless ( $itemnumber );
1962 my $dbh = C4::Context->dbh;
1964 ## Get the waiting reserve we want to revert
1965 my $query = "
1966 SELECT * FROM reserves
1967 WHERE itemnumber = ?
1968 AND found IS NOT NULL
1970 my $sth = $dbh->prepare( $query );
1971 $sth->execute( $itemnumber );
1972 my $reserve = $sth->fetchrow_hashref();
1974 my $hold = Koha::Holds->find( $reserve->{reserve_id} ); # TODO Remove the next raw SQL statements and use this instead
1976 ## Increment the priority of all other non-waiting
1977 ## reserves for this bib record
1978 $query = "
1979 UPDATE reserves
1981 priority = priority + 1
1982 WHERE
1983 biblionumber = ?
1985 priority > 0
1987 $sth = $dbh->prepare( $query );
1988 $sth->execute( $reserve->{'biblionumber'} );
1990 $hold->set(
1992 priority => 1,
1993 found => undef,
1994 waitingdate => undef,
1995 itemnumber => $hold->item_level_hold ? $hold->itemnumber : undef,
1997 )->store();
1999 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2001 return $hold;
2004 =head2 ReserveSlip
2006 ReserveSlip(
2008 branchcode => $branchcode,
2009 borrowernumber => $borrowernumber,
2010 biblionumber => $biblionumber,
2011 [ itemnumber => $itemnumber, ]
2012 [ barcode => $barcode, ]
2016 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2018 The letter code will be HOLD_SLIP, and the following tables are
2019 available within the slip:
2021 reserves
2022 branches
2023 borrowers
2024 biblio
2025 biblioitems
2026 items
2028 =cut
2030 sub ReserveSlip {
2031 my ($args) = @_;
2032 my $branchcode = $args->{branchcode};
2033 my $borrowernumber = $args->{borrowernumber};
2034 my $biblionumber = $args->{biblionumber};
2035 my $itemnumber = $args->{itemnumber};
2036 my $barcode = $args->{barcode};
2039 my $patron = Koha::Patrons->find($borrowernumber);
2041 my $hold;
2042 if ($itemnumber || $barcode ) {
2043 $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
2045 $hold = Koha::Holds->search(
2047 biblionumber => $biblionumber,
2048 borrowernumber => $borrowernumber,
2049 itemnumber => $itemnumber
2051 )->next;
2053 else {
2054 $hold = Koha::Holds->search(
2056 biblionumber => $biblionumber,
2057 borrowernumber => $borrowernumber
2059 )->next;
2062 return unless $hold;
2063 my $reserve = $hold->unblessed;
2065 return C4::Letters::GetPreparedLetter (
2066 module => 'circulation',
2067 letter_code => 'HOLD_SLIP',
2068 branchcode => $branchcode,
2069 lang => $patron->lang,
2070 tables => {
2071 'reserves' => $reserve,
2072 'branches' => $reserve->{branchcode},
2073 'borrowers' => $reserve->{borrowernumber},
2074 'biblio' => $reserve->{biblionumber},
2075 'biblioitems' => $reserve->{biblionumber},
2076 'items' => $reserve->{itemnumber},
2081 =head2 GetReservesControlBranch
2083 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2085 Return the branchcode to be used to determine which reserves
2086 policy applies to a transaction.
2088 C<$item> is a hashref for an item. Only 'homebranch' is used.
2090 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2092 =cut
2094 sub GetReservesControlBranch {
2095 my ( $item, $borrower ) = @_;
2097 my $reserves_control = C4::Context->preference('ReservesControlBranch');
2099 my $branchcode =
2100 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2101 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2102 : undef;
2104 return $branchcode;
2107 =head2 CalculatePriority
2109 my $p = CalculatePriority($biblionumber, $resdate);
2111 Calculate priority for a new reserve on biblionumber, placing it at
2112 the end of the line of all holds whose start date falls before
2113 the current system time and that are neither on the hold shelf
2114 or in transit.
2116 The reserve date parameter is optional; if it is supplied, the
2117 priority is based on the set of holds whose start date falls before
2118 the parameter value.
2120 After calculation of this priority, it is recommended to call
2121 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2122 AddReserves.
2124 =cut
2126 sub CalculatePriority {
2127 my ( $biblionumber, $resdate ) = @_;
2129 my $sql = q{
2130 SELECT COUNT(*) FROM reserves
2131 WHERE biblionumber = ?
2132 AND priority > 0
2133 AND (found IS NULL OR found = '')
2135 #skip found==W or found==T (waiting or transit holds)
2136 if( $resdate ) {
2137 $sql.= ' AND ( reservedate <= ? )';
2139 else {
2140 $sql.= ' AND ( reservedate < NOW() )';
2142 my $dbh = C4::Context->dbh();
2143 my @row = $dbh->selectrow_array(
2144 $sql,
2145 undef,
2146 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2149 return @row ? $row[0]+1 : 1;
2152 =head2 IsItemOnHoldAndFound
2154 my $bool = IsItemFoundHold( $itemnumber );
2156 Returns true if the item is currently on hold
2157 and that hold has a non-null found status ( W, T, etc. )
2159 =cut
2161 sub IsItemOnHoldAndFound {
2162 my ($itemnumber) = @_;
2164 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2166 my $found = $rs->count(
2168 itemnumber => $itemnumber,
2169 found => { '!=' => undef }
2173 return $found;
2176 =head2 GetMaxPatronHoldsForRecord
2178 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2180 For multiple holds on a given record for a given patron, the max
2181 number of record level holds that a patron can be placed is the highest
2182 value of the holds_per_record rule for each item if the record for that
2183 patron. This subroutine finds and returns the highest holds_per_record
2184 rule value for a given patron id and record id.
2186 =cut
2188 sub GetMaxPatronHoldsForRecord {
2189 my ( $borrowernumber, $biblionumber ) = @_;
2191 my $patron = Koha::Patrons->find($borrowernumber);
2192 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2194 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2196 my $categorycode = $patron->categorycode;
2197 my $branchcode;
2198 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2200 my $max = 0;
2201 foreach my $item (@items) {
2202 my $itemtype = $item->effective_itemtype();
2204 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2206 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2207 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2208 $max = $holds_per_record if $holds_per_record > $max;
2211 return $max;
2214 =head2 GetHoldRule
2216 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2218 Returns the matching hold related issuingrule fields for a given
2219 patron category, itemtype, and library.
2221 =cut
2223 sub GetHoldRule {
2224 my ( $categorycode, $itemtype, $branchcode ) = @_;
2226 my $reservesallowed = Koha::CirculationRules->get_effective_rule(
2228 itemtype => $itemtype,
2229 categorycode => $categorycode,
2230 branchcode => $branchcode,
2231 rule_name => 'reservesallowed',
2232 order_by => {
2233 -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
2238 my $rules;
2239 if ( $reservesallowed ) {
2240 $rules->{reservesallowed} = $reservesallowed->rule_value;
2241 $rules->{itemtype} = $reservesallowed->itemtype;
2242 $rules->{categorycode} = $reservesallowed->categorycode;
2243 $rules->{branchcode} = $reservesallowed->branchcode;
2246 my $holds_per_x_rules = Koha::CirculationRules->get_effective_rules(
2248 itemtype => $itemtype,
2249 categorycode => $categorycode,
2250 branchcode => $branchcode,
2251 rules => ['holds_per_record', 'holds_per_day'],
2252 order_by => {
2253 -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
2257 $rules->{holds_per_record} = $holds_per_x_rules->{holds_per_record};
2258 $rules->{holds_per_day} = $holds_per_x_rules->{holds_per_day};
2260 return $rules;
2263 =head1 AUTHOR
2265 Koha Development Team <http://koha-community.org/>
2267 =cut