Bug 29915: (QA follow-up) Fix POD typo
[koha.git] / C4 / Reserves.pm
1 package C4::Reserves;
2
3 # Copyright 2000-2002 Katipo Communications
4 #           2006 SAN Ouest Provence
5 #           2007-2010 BibLibre Paul POULAIN
6 #           2011 Catalyst IT
7 #
8 # This file is part of Koha.
9 #
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.
14 #
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.
19 #
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>.
22
23
24 use Modern::Perl;
25
26 use C4::Accounts;
27 use C4::Circulation qw( CheckIfIssuedToPatron GetAgeRestriction GetBranchItemRule );
28 use C4::Context;
29 use C4::Items qw( CartToShelf get_hostitemnumbers_of );
30 use C4::Letters;
31 use C4::Log qw( logaction );
32 use C4::Members::Messaging;
33 use C4::Members;
34 use Koha::Account::Lines;
35 use Koha::Biblios;
36 use Koha::Calendar;
37 use Koha::CirculationRules;
38 use Koha::Database;
39 use Koha::DateUtils qw( dt_from_string output_pref );
40 use Koha::Hold;
41 use Koha::Holds;
42 use Koha::ItemTypes;
43 use Koha::Items;
44 use Koha::Libraries;
45 use Koha::Old::Hold;
46 use Koha::Patrons;
47 use Koha::Plugins;
48
49 use List::MoreUtils qw( any );
50
51 =head1 NAME
52
53 C4::Reserves - Koha functions for dealing with reservation.
54
55 =head1 SYNOPSIS
56
57   use C4::Reserves;
58
59 =head1 DESCRIPTION
60
61 This modules provides somes functions to deal with reservations.
62
63   Reserves are stored in reserves table.
64   The following columns contains important values :
65   - priority >0      : then the reserve is at 1st stage, and not yet affected to any item.
66              =0      : then the reserve is being dealed
67   - found : NULL         : means the patron requested the 1st available, and we haven't chosen the item
68             T(ransit)    : the reserve is linked to an item but is in transit to the pickup branch
69             W(aiting)    : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
70             F(inished)   : the reserve has been completed, and is done
71             P(rocessing) : reserved item has been returned using self-check machine and reserve needs to be confirmed
72                            by librarian before notice is send and status changed to waiting.
73                            Applicable only if HoldsNeedProcessingSIP system preference is set.
74   - itemnumber : empty : the reserve is still unaffected to an item
75                  filled: the reserve is attached to an item
76   The complete workflow is :
77   ==== 1st use case ====
78   patron request a document, 1st available :                      P >0, F=NULL, I=NULL
79   a library having it run "transfertodo", and clic on the list
80          if there is no transfer to do, the reserve waiting
81          patron can pick it up                                    P =0, F=W,    I=filled
82          if there is a transfer to do, write in branchtransfer    P =0, F=T,    I=filled
83            The pickup library receive the book, it check in       P =0, F=W,    I=filled
84   The patron borrow the book                                      P =0, F=F,    I=filled
85
86   ==== 2nd use case ====
87   patron requests a document, a given item,
88     If pickup is holding branch                                   P =0, F=W,   I=filled
89     If transfer needed, write in branchtransfer                   P =0, F=T,    I=filled
90         The pickup library receive the book, it checks it in      P =0, F=W,    I=filled
91   The patron borrow the book                                      P =0, F=F,    I=filled
92
93 =head1 FUNCTIONS
94
95 =cut
96
97 our (@ISA, @EXPORT_OK);
98 BEGIN {
99     require Exporter;
100     @ISA = qw(Exporter);
101     @EXPORT_OK = qw(
102       AddReserve
103
104       GetReserveStatus
105
106       GetOtherReserves
107       ChargeReserveFee
108       GetReserveFee
109
110       ModReserveAffect
111       ModReserve
112       ModReserveStatus
113       ModReserveCancelAll
114       ModReserveMinusPriority
115       MoveReserve
116
117       CheckReserves
118       CanBookBeReserved
119       CanItemBeReserved
120       CanReserveBeCanceledFromOpac
121       CancelExpiredReserves
122
123       AutoUnsuspendReserves
124
125       IsAvailableForItemLevelRequest
126       ItemsAnyAvailableAndNotRestricted
127
128       AlterPriority
129       ToggleLowestPriority
130
131       ReserveSlip
132       ToggleSuspend
133       SuspendAll
134
135       GetReservesControlBranch
136
137       CalculatePriority
138
139       IsItemOnHoldAndFound
140
141       GetMaxPatronHoldsForRecord
142
143       MergeHolds
144
145       RevertWaitingStatus
146     );
147 }
148
149 =head2 AddReserve
150
151     AddReserve(
152         {
153             branchcode       => $branchcode,
154             borrowernumber   => $borrowernumber,
155             biblionumber     => $biblionumber,
156             priority         => $priority,
157             reservation_date => $reservation_date,
158             expiration_date  => $expiration_date,
159             notes            => $notes,
160             title            => $title,
161             itemnumber       => $itemnumber,
162             found            => $found,
163             itemtype         => $itemtype,
164         }
165     );
166
167 Adds reserve and generates HOLDPLACED message.
168
169 The following tables are available witin the HOLDPLACED message:
170
171     branches
172     borrowers
173     biblio
174     biblioitems
175     items
176     reserves
177
178 =cut
179
180 sub AddReserve {
181     my ($params)       = @_;
182     my $branch         = $params->{branchcode};
183     my $borrowernumber = $params->{borrowernumber};
184     my $biblionumber   = $params->{biblionumber};
185     my $priority       = $params->{priority};
186     my $resdate        = $params->{reservation_date};
187     my $patron_expiration_date = $params->{expiration_date};
188     my $notes          = $params->{notes};
189     my $title          = $params->{title};
190     my $checkitem      = $params->{itemnumber};
191     my $found          = $params->{found};
192     my $itemtype       = $params->{itemtype};
193     my $non_priority   = $params->{non_priority};
194
195     $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
196         or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
197
198     $patron_expiration_date = output_pref({ str => $patron_expiration_date, dateonly => 1, dateformat => 'iso' });
199
200     # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
201     # of the document, we force the value $priority and $found .
202     if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
203         my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
204
205         if (
206             # If item is already checked out, it cannot be set waiting
207             !$item->onloan
208
209             # The item can't be waiting if it needs a transfer
210             && $item->holdingbranch eq $branch
211
212             # Similarly, if in transit it can't be waiting
213             && !$item->get_transfer
214
215             # If we can't hold damaged items, and it is damaged, it can't be waiting
216             && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
217
218             # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
219             && !$item->current_holds->count )
220         {
221             $priority = 0;
222             $found = 'W';
223         }
224     }
225
226     if ( C4::Context->preference('AllowHoldDateInFuture') ) {
227
228         # Make room in reserves for this before those of a later reserve date
229         $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
230     }
231
232     my $waitingdate;
233
234     # If the reserv had the waiting status, we had the value of the resdate
235     if ( $found && $found eq 'W' ) {
236         $waitingdate = $resdate;
237     }
238
239     # Don't add itemtype limit if specific item is selected
240     $itemtype = undef if $checkitem;
241
242     # updates take place here
243     my $hold = Koha::Hold->new(
244         {
245             borrowernumber => $borrowernumber,
246             biblionumber   => $biblionumber,
247             reservedate    => $resdate,
248             branchcode     => $branch,
249             priority       => $priority,
250             reservenotes   => $notes,
251             itemnumber     => $checkitem,
252             found          => $found,
253             waitingdate    => $waitingdate,
254             patron_expiration_date => $patron_expiration_date,
255             itemtype       => $itemtype,
256             item_level_hold => $checkitem ? 1 : 0,
257             non_priority   => $non_priority ? 1 : 0,
258         }
259     )->store();
260     $hold->set_waiting() if $found && $found eq 'W';
261
262     logaction( 'HOLDS', 'CREATE', $hold->id, $hold )
263         if C4::Context->preference('HoldsLog');
264
265     my $reserve_id = $hold->id();
266
267     # add a reserve fee if needed
268     if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
269         my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
270         ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
271     }
272
273     _FixPriority({ biblionumber => $biblionumber});
274
275     # Send e-mail to librarian if syspref is active
276     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
277         my $patron = Koha::Patrons->find( $borrowernumber );
278         my $library = $patron->library;
279         if ( my $letter =  C4::Letters::GetPreparedLetter (
280             module => 'reserves',
281             letter_code => 'HOLDPLACED',
282             branchcode => $branch,
283             lang => $patron->lang,
284             tables => {
285                 'branches'    => $library->unblessed,
286                 'borrowers'   => $patron->unblessed,
287                 'biblio'      => $biblionumber,
288                 'biblioitems' => $biblionumber,
289                 'items'       => $checkitem,
290                 'reserves'    => $hold->unblessed,
291             },
292         ) ) {
293
294             my $branch_email_address = $library->inbound_email_address;
295
296             C4::Letters::EnqueueLetter(
297                 {
298                     letter                 => $letter,
299                     borrowernumber         => $borrowernumber,
300                     message_transport_type => 'email',
301                     to_address             => $branch_email_address,
302                 }
303             );
304         }
305     }
306
307     Koha::Plugins->call('after_hold_create', $hold);
308
309     return $reserve_id;
310 }
311
312 =head2 CanBookBeReserved
313
314   $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode, $params)
315   if ($canReserve eq 'OK') { #We can reserve this Item! }
316
317   $params are passed directly through to CanItemBeReserved
318
319 See CanItemBeReserved() for possible return values.
320
321 =cut
322
323 sub CanBookBeReserved{
324     my ($borrowernumber, $biblionumber, $pickup_branchcode, $params) = @_;
325
326     # Check that patron have not checked out this biblio (if AllowHoldsOnPatronsPossessions set)
327     if ( !C4::Context->preference('AllowHoldsOnPatronsPossessions')
328         && C4::Circulation::CheckIfIssuedToPatron( $borrowernumber, $biblionumber ) ) {
329         return { status =>'alreadypossession' };
330     }
331
332     my $items;
333     #get items linked via host records
334     my @hostitemnumbers = get_hostitemnumbers_of($biblionumber);
335     if (@hostitemnumbers){
336         $items = Koha::Items->search({
337             -or => [
338                 biblionumber => $biblionumber,
339                 itemnumber => { -in => @hostitemnumbers }
340             ]
341         });
342     } else {
343         $items = Koha::Items->search({ biblionumber => $biblionumber});
344     }
345
346     my $canReserve = { status => '' };
347     my $patron = Koha::Patrons->find( $borrowernumber );
348     while ( my $item = $items->next ) {
349         $canReserve = CanItemBeReserved( $patron, $item, $pickup_branchcode, $params );
350         return { status => 'OK' } if $canReserve->{status} eq 'OK';
351     }
352     return $canReserve;
353 }
354
355 =head2 CanItemBeReserved
356
357   $canReserve = &CanItemBeReserved($patron, $item, $branchcode, $params)
358   if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
359
360   current params are:
361   'ignore_found_holds' - if true holds that have been trapped are not counted
362   toward the patron limit, used by checkHighHolds to avoid counting the hold we will fill with the
363   current checkout against the high holds threshold
364   'ignore_hold_counts' - we use this routine to check if an item can fill a hold - on this case we
365   should not check if there are too many holds as we only csre about reservability
366
367 @RETURNS { status => OK },              if the Item can be reserved.
368          { status => ageRestricted },   if the Item is age restricted for this borrower.
369          { status => damaged },         if the Item is damaged.
370          { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
371          { status => branchNotInHoldGroup }, if borrower home library is not in hold group, and holds are only allowed from hold groups.
372          { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
373          { status => notReservable },   if holds on this item are not allowed
374          { status => libraryNotFound },   if given branchcode is not an existing library
375          { status => libraryNotPickupLocation },   if given branchcode is not configured to be a pickup location
376          { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
377          { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
378          { status => recall }, if the borrower has already placed a recall on this item
379
380 =cut
381
382 sub CanItemBeReserved {
383     my ( $patron, $item, $pickup_branchcode, $params ) = @_;
384
385     my $dbh = C4::Context->dbh;
386     my $ruleitemtype;    # itemtype of the matching issuing rule
387     my $allowedreserves  = 0; # Total number of holds allowed across all records, default to none
388
389     # We check item branch if IndependentBranches is ON
390     # and canreservefromotherbranches is OFF
391     if ( C4::Context->preference('IndependentBranches')
392         and !C4::Context->preference('canreservefromotherbranches') )
393     {
394         if ( $item->homebranch ne $patron->branchcode ) {
395             return { status => 'cannotReserveFromOtherBranches' };
396         }
397     }
398
399     # we retrieve borrowers and items informations #
400     # item->{itype} will come for biblioitems if necessery
401     my $biblio   = $item->biblio;
402     my $borrower = $patron->unblessed;
403
404     # If an item is damaged and we don't allow holds on damaged items, we can stop right here
405     return { status =>'damaged' }
406       if ( $item->damaged
407         && !C4::Context->preference('AllowHoldsOnDamagedItems') );
408
409     # Check for the age restriction
410     my ( $ageRestriction, $daysToAgeRestriction ) =
411       C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
412     return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
413
414     # Check that the patron doesn't have an item level hold on this item already
415     return { status =>'itemAlreadyOnHold' }
416       if ( !$params->{ignore_hold_counts} && Koha::Holds->search( { borrowernumber => $patron->borrowernumber, itemnumber => $item->itemnumber } )->count() );
417
418     # Check that patron have not checked out this biblio (if AllowHoldsOnPatronsPossessions set)
419     if ( !C4::Context->preference('AllowHoldsOnPatronsPossessions')
420         && C4::Circulation::CheckIfIssuedToPatron( $patron->borrowernumber, $biblio->biblionumber ) ) {
421         return { status =>'alreadypossession' };
422     }
423
424     # check if a recall exists on this item from this borrower
425     return { status => 'recall' }
426       if $patron->recalls->filter_by_current->search({ itemnumber => $item->itemnumber })->count;
427
428     my $controlbranch = C4::Context->preference('ReservesControlBranch');
429
430     my $reserves_control_branch;
431     my $branchfield = "reserves.branchcode";
432
433     if ( $controlbranch eq "ItemHomeLibrary" ) {
434         $branchfield = "items.homebranch";
435         $reserves_control_branch  = $item->homebranch;
436     }
437     elsif ( $controlbranch eq "PatronLibrary" ) {
438         $branchfield = "borrowers.branchcode";
439         $reserves_control_branch  = $borrower->{branchcode};
440     }
441
442     # we retrieve rights
443     if (
444         my $reservesallowed = Koha::CirculationRules->get_effective_rule({
445                 itemtype     => $item->effective_itemtype,
446                 categorycode => $borrower->{categorycode},
447                 branchcode   => $reserves_control_branch,
448                 rule_name    => 'reservesallowed',
449         })
450     ) {
451         $ruleitemtype     = $reservesallowed->itemtype;
452         $allowedreserves  = $reservesallowed->rule_value // 0; #undefined is 0, blank is unlimited
453     }
454     else {
455         $ruleitemtype = undef;
456     }
457
458     my $rights = Koha::CirculationRules->get_effective_rules({
459         categorycode => $borrower->{'categorycode'},
460         itemtype     => $item->effective_itemtype,
461         branchcode   => $reserves_control_branch,
462         rules        => ['holds_per_record','holds_per_day']
463     });
464     my $holds_per_record = $rights->{holds_per_record} // 1;
465     my $holds_per_day    = $rights->{holds_per_day};
466
467     if (   defined $holds_per_record && $holds_per_record ne '' ){
468         if ( $holds_per_record == 0 ) {
469             return { status => "noReservesAllowed" };
470         }
471         if ( !$params->{ignore_hold_counts} ) {
472             my $search_params = {
473                 borrowernumber => $patron->borrowernumber,
474                 biblionumber   => $item->biblionumber,
475             };
476             $search_params->{found} = undef if $params->{ignore_found_holds};
477             my $holds = Koha::Holds->search($search_params);
478             return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record } if $holds->count() >= $holds_per_record;
479         }
480     }
481
482     if (!$params->{ignore_hold_counts} && defined $holds_per_day && $holds_per_day ne '')
483     {
484         my $today_holds = Koha::Holds->search({
485             borrowernumber => $patron->borrowernumber,
486             reservedate    => dt_from_string->date
487         });
488         return { status => 'tooManyReservesToday', limit => $holds_per_day } if $today_holds->count() >= $holds_per_day;
489     }
490
491     # we check if it's ok or not
492     if ( defined $allowedreserves && $allowedreserves ne '' ){
493         if( $allowedreserves == 0 ){
494             return { status => 'noReservesAllowed' };
495         }
496         if ( !$params->{ignore_hold_counts} ) {
497             # we retrieve count
498             my $querycount = q{
499                 SELECT count(*) AS count
500                   FROM reserves
501              LEFT JOIN items USING (itemnumber)
502              LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
503              LEFT JOIN borrowers USING (borrowernumber)
504                  WHERE borrowernumber = ?
505             };
506             $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
507
508             # If using item-level itypes, fall back to the record
509             # level itemtype if the hold has no associated item
510             $querycount .=
511               C4::Context->preference('item-level_itypes')
512               ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
513               : " AND biblioitems.itemtype = ?"
514               if defined $ruleitemtype;
515
516             my $sthcount = $dbh->prepare($querycount);
517
518             if ( defined $ruleitemtype ) {
519                 $sthcount->execute( $patron->borrowernumber, $reserves_control_branch, $ruleitemtype );
520             }
521             else {
522                 $sthcount->execute( $patron->borrowernumber, $reserves_control_branch );
523             }
524
525             my $reservecount = "0";
526             if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
527                 $reservecount = $rowcount->{count};
528             }
529
530             return { status => 'tooManyReserves', limit => $allowedreserves } if $reservecount >= $allowedreserves;
531         }
532     }
533
534     # Now we need to check hold limits by patron category
535     my $rule = Koha::CirculationRules->get_effective_rule(
536         {
537             categorycode => $patron->categorycode,
538             branchcode   => $reserves_control_branch,
539             rule_name    => 'max_holds',
540         }
541     );
542     if (!$params->{ignore_hold_counts} && $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
543         my $total_holds_count = Koha::Holds->search(
544             {
545                 borrowernumber => $patron->borrowernumber
546             }
547         )->count();
548
549         return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
550     }
551
552     my $branchitemrule =
553       C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->effective_itemtype );
554
555     if ( $branchitemrule->{holdallowed} eq 'not_allowed' ) {
556         return { status => 'notReservable' };
557     }
558
559     if (   $branchitemrule->{holdallowed} eq 'from_home_library'
560         && $borrower->{branchcode} ne $item->homebranch )
561     {
562         return { status => 'cannotReserveFromOtherBranches' };
563     }
564
565     my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
566     if ( $branchitemrule->{holdallowed} eq 'from_local_hold_group') {
567         if($patron->branchcode ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $patron->branchcode} )) {
568             return { status => 'branchNotInHoldGroup' };
569         }
570     }
571
572     if ($pickup_branchcode) {
573         my $destination = Koha::Libraries->find({
574             branchcode => $pickup_branchcode,
575         });
576
577         unless ($destination) {
578             return { status => 'libraryNotFound' };
579         }
580         unless ($destination->pickup_location) {
581             return { status => 'libraryNotPickupLocation' };
582         }
583         unless ($item->can_be_transferred({ to => $destination })) {
584             return { status => 'cannotBeTransferred' };
585         }
586         if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup' && !$item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
587             return { status => 'pickupNotInHoldGroup' };
588         }
589         if ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup' && !Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
590             return { status => 'pickupNotInHoldGroup' };
591         }
592     }
593
594     return { status => 'OK' };
595 }
596
597 =head2 CanReserveBeCanceledFromOpac
598
599     $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
600
601     returns 1 if reserve can be cancelled by user from OPAC.
602     First check if reserve belongs to user, next checks if reserve is not in
603     transfer or waiting status
604
605 =cut
606
607 sub CanReserveBeCanceledFromOpac {
608     my ($reserve_id, $borrowernumber) = @_;
609
610     return unless $reserve_id and $borrowernumber;
611     my $reserve = Koha::Holds->find($reserve_id) or return;
612
613     return 0 unless $reserve->borrowernumber == $borrowernumber;
614     return $reserve->is_cancelable_from_opac;
615 }
616
617 =head2 GetOtherReserves
618
619   ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
620
621 Check queued list of this document and check if this document must be transferred
622
623 =cut
624
625 sub GetOtherReserves {
626     my ($itemnumber) = @_;
627     my $messages;
628     my $nextreservinfo;
629     my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
630     if ($checkreserves) {
631         my $item = Koha::Items->find($itemnumber);
632         if ( $item->holdingbranch ne $checkreserves->{'branchcode'} ) {
633             $messages->{'transfert'} = $checkreserves->{'branchcode'};
634             #minus priorities of others reservs
635             ModReserveMinusPriority(
636                 $itemnumber,
637                 $checkreserves->{'reserve_id'},
638             );
639
640             #launch the subroutine dotransfer
641             C4::Items::ModItemTransfer(
642                 $itemnumber,
643                 $item->holdingbranch,
644                 $checkreserves->{'branchcode'},
645                 'Reserve'
646               ),
647               ;
648         }
649
650      #step 2b : case of a reservation on the same branch, set the waiting status
651         else {
652             $messages->{'waiting'} = 1;
653             ModReserveMinusPriority(
654                 $itemnumber,
655                 $checkreserves->{'reserve_id'},
656             );
657             ModReserveStatus($itemnumber,'W');
658         }
659
660         $nextreservinfo = $checkreserves;
661     }
662
663     return ( $messages, $nextreservinfo );
664 }
665
666 =head2 ChargeReserveFee
667
668     $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
669
670     Charge the fee for a reserve (if $fee > 0)
671
672 =cut
673
674 sub ChargeReserveFee {
675     my ( $borrowernumber, $fee, $title ) = @_;
676     return if !$fee || $fee == 0;    # the last test is needed to include 0.00
677     Koha::Account->new( { patron_id => $borrowernumber } )->add_debit(
678         {
679             amount       => $fee,
680             description  => $title,
681             note         => undef,
682             user_id      => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
683             library_id   => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
684             interface    => C4::Context->interface,
685             invoice_type => undef,
686             type         => 'RESERVE',
687             item_id      => undef
688         }
689     );
690 }
691
692 =head2 GetReserveFee
693
694     $fee = GetReserveFee( $borrowernumber, $biblionumber );
695
696     Calculate the fee for a reserve (if applicable).
697
698 =cut
699
700 sub GetReserveFee {
701     my ( $borrowernumber, $biblionumber ) = @_;
702     my $borquery = qq{
703 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
704     };
705     my $issue_qry = qq{
706 SELECT COUNT(*) FROM items
707 LEFT JOIN issues USING (itemnumber)
708 WHERE items.biblionumber=? AND issues.issue_id IS NULL
709     };
710     my $holds_qry = qq{
711 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
712     };
713
714     my $dbh = C4::Context->dbh;
715     my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
716     my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
717     if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
718         # This is a reconstruction of the old code:
719         # Compare number of items with items issued, and optionally check holds
720         # If not all items are issued and there are no holds: charge no fee
721         # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
722         my ( $notissued, $reserved );
723         ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
724             ( $biblionumber ) );
725         if( $notissued ) {
726             ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
727                 ( $biblionumber, $borrowernumber ) );
728             $fee = 0 if $reserved == 0;
729         }
730     }
731     return $fee;
732 }
733
734 =head2 GetReserveStatus
735
736   $reservestatus = GetReserveStatus($itemnumber);
737
738 Takes an itemnumber and returns the status of the reserve placed on it.
739 If several reserves exist, the reserve with the lower priority is given.
740
741 =cut
742
743 ## FIXME: I don't think this does what it thinks it does.
744 ## It only ever checks the first reserve result, even though
745 ## multiple reserves for that bib can have the itemnumber set
746 ## the sub is only used once in the codebase.
747 sub GetReserveStatus {
748     my ($itemnumber) = @_;
749
750     my $dbh = C4::Context->dbh;
751
752     my ($sth, $found, $priority);
753     if ( $itemnumber ) {
754         $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
755         $sth->execute($itemnumber);
756         ($found, $priority) = $sth->fetchrow_array;
757     }
758
759     if(defined $found) {
760         return 'Waiting'  if $found eq 'W' and $priority == 0;
761         return 'Processing'  if $found eq 'P';
762         return 'Finished' if $found eq 'F';
763     }
764
765     return 'Reserved' if defined $priority && $priority > 0;
766
767     return ''; # empty string here will remove need for checking undef, or less log lines
768 }
769
770 =head2 CheckReserves
771
772   ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber);
773   ($status, $matched_reserve, $possible_reserves) = &CheckReserves(undef, $barcode);
774   ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
775
776 Find a book in the reserves.
777
778 C<$itemnumber> is the book's item number.
779 C<$lookahead> is the number of days to look in advance for future reserves.
780
781 As I understand it, C<&CheckReserves> looks for the given item in the
782 reserves. If it is found, that's a match, and C<$status> is set to
783 C<Waiting>.
784
785 Otherwise, it finds the most important item in the reserves with the
786 same biblio number as this book (I'm not clear on this) and returns it
787 with C<$status> set to C<Reserved>.
788
789 C<&CheckReserves> returns a two-element list:
790
791 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
792
793 C<$reserve> is the reserve item that matched. It is a
794 reference-to-hash whose keys are mostly the fields of the reserves
795 table in the Koha database.
796
797 =cut
798
799 sub CheckReserves {
800     my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
801     my $dbh = C4::Context->dbh;
802     my $sth;
803     my $select;
804     if (C4::Context->preference('item-level_itypes')){
805         $select = "
806            SELECT items.biblionumber,
807            items.biblioitemnumber,
808            itemtypes.notforloan,
809            items.notforloan AS itemnotforloan,
810            items.itemnumber,
811            items.damaged,
812            items.homebranch,
813            items.holdingbranch
814            FROM   items
815            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
816            LEFT JOIN itemtypes   ON items.itype   = itemtypes.itemtype
817         ";
818     }
819     else {
820         $select = "
821            SELECT items.biblionumber,
822            items.biblioitemnumber,
823            itemtypes.notforloan,
824            items.notforloan AS itemnotforloan,
825            items.itemnumber,
826            items.damaged,
827            items.homebranch,
828            items.holdingbranch
829            FROM   items
830            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
831            LEFT JOIN itemtypes   ON biblioitems.itemtype   = itemtypes.itemtype
832         ";
833     }
834
835     if ($item) {
836         $sth = $dbh->prepare("$select WHERE itemnumber = ?");
837         $sth->execute($item);
838     }
839     else {
840         $sth = $dbh->prepare("$select WHERE barcode = ?");
841         $sth->execute($barcode);
842     }
843     # note: we get the itemnumber because we might have started w/ just the barcode.  Now we know for sure we have it.
844     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
845     return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
846
847     return unless $itemnumber; # bail if we got nothing.
848     # if item is not for loan it cannot be reserved either.....
849     # except where items.notforloan < 0 :  This indicates the item is holdable.
850
851     my @SkipHoldTrapOnNotForLoanValue = split( '\|', C4::Context->preference('SkipHoldTrapOnNotForLoanValue') );
852     return if grep { $_ eq $notforloan_per_item } @SkipHoldTrapOnNotForLoanValue;
853
854     my $dont_trap = C4::Context->preference('TrapHoldsOnOrder') ? ($notforloan_per_item > 0) : ($notforloan_per_item && 1 );
855     return if $dont_trap or $notforloan_per_itemtype;
856
857     # Find this item in the reserves
858     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
859
860     # $priority and $highest are used to find the most important item
861     # in the list returned by &_Findgroupreserve. (The lower $priority,
862     # the more important the item.)
863     # $highest is the most important item we've seen so far.
864     my $highest;
865
866     if (scalar @reserves) {
867         my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
868         my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
869         my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
870
871         my $priority = 10000000;
872         foreach my $res (@reserves) {
873             if ($res->{'found'} && $res->{'found'} eq 'W') {
874                 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
875             } elsif ($res->{'found'} && $res->{'found'} eq 'P') {
876                 return ( "Processing", $res, \@reserves ); # Found determinated hold, e. g. the transferred one
877             } elsif ($res->{'found'} && $res->{'found'} eq 'T') {
878                 return ( "Transferred", $res, \@reserves ); # Found determinated hold, e. g. the transferred one
879             } else {
880                 my $patron;
881                 my $item;
882                 my $local_hold_match;
883
884                 if ($LocalHoldsPriority) {
885                     $patron = Koha::Patrons->find( $res->{borrowernumber} );
886                     $item = Koha::Items->find($itemnumber);
887
888                     unless ($item->exclude_from_local_holds_priority || $patron->category->exclude_from_local_holds_priority) {
889                         my $local_holds_priority_item_branchcode =
890                             $item->$LocalHoldsPriorityItemControl;
891                         my $local_holds_priority_patron_branchcode =
892                             ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
893                             ? $res->{branchcode}
894                             : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
895                             ? $patron->branchcode
896                             : undef;
897                         $local_hold_match =
898                             $local_holds_priority_item_branchcode eq
899                             $local_holds_priority_patron_branchcode;
900                     }
901                 }
902
903                 # See if this item is more important than what we've got so far
904                 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
905                     $item ||= Koha::Items->find($itemnumber);
906                     next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
907                     $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
908                     my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
909                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
910                     next if ($branchitemrule->{'holdallowed'} eq 'not_allowed');
911                     next if (($branchitemrule->{'holdallowed'} eq 'from_home_library') && ($item->homebranch ne $patron->branchcode));
912                     my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
913                     next if (($branchitemrule->{'holdallowed'} eq 'from_local_hold_group') && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
914                     my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
915                     next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
916                     next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
917                     next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
918                     next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
919                     $priority = $res->{'priority'};
920                     $highest  = $res;
921                     last if $local_hold_match;
922                 }
923             }
924         }
925     }
926
927     # If we get this far, then no exact match was found.
928     # We return the most important (i.e. next) reservation.
929     if ($highest) {
930         $highest->{'itemnumber'} = $item;
931         return ( "Reserved", $highest, \@reserves );
932     }
933
934     return ( '' );
935 }
936
937 =head2 CancelExpiredReserves
938
939   CancelExpiredReserves();
940
941 Cancels all reserves with an expiration date from before today.
942
943 =cut
944
945 sub CancelExpiredReserves {
946     my $cancellation_reason = shift;
947     my $today = dt_from_string();
948     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
949     my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
950
951     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
952     my $params = {
953         -or => [
954             { expirationdate => { '<', $dtf->format_date($today) } },
955             { patron_expiration_date => { '<' => $dtf->format_date($today) } }
956         ]
957     };
958
959     $params->{found} = [ { '!=', 'W' }, undef ]  unless $expireWaiting;
960
961     # FIXME To move to Koha::Holds->search_expired (?)
962     my $holds = Koha::Holds->search( $params );
963
964     while ( my $hold = $holds->next ) {
965         my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
966
967         next if !$cancel_on_holidays && $calendar->is_holiday( $today );
968
969         my $cancel_params = {};
970         $cancel_params->{cancellation_reason} = $cancellation_reason if defined($cancellation_reason);
971         if ( defined($hold->found) && $hold->found eq 'W' ) {
972             $cancel_params->{charge_cancel_fee} = 1;
973         }
974         $hold->cancel( $cancel_params );
975     }
976 }
977
978 =head2 AutoUnsuspendReserves
979
980   AutoUnsuspendReserves();
981
982 Unsuspends all suspended reserves with a suspend_until date from before today.
983
984 =cut
985
986 sub AutoUnsuspendReserves {
987     my $today = dt_from_string();
988
989     my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } )->as_list;
990
991     map { $_->resume() } @holds;
992 }
993
994 =head2 ModReserve
995
996   ModReserve({ rank => $rank,
997                reserve_id => $reserve_id,
998                branchcode => $branchcode
999                [, itemnumber => $itemnumber ]
1000                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1001               });
1002
1003 Change a hold request's priority or cancel it.
1004
1005 C<$rank> specifies the effect of the change.  If C<$rank>
1006 is 'n', nothing happens.  This corresponds to leaving a
1007 request alone when changing its priority in the holds queue
1008 for a bib.
1009
1010 If C<$rank> is 'del', the hold request is cancelled.
1011
1012 If C<$rank> is an integer greater than zero, the priority of
1013 the request is set to that value.  Since priority != 0 means
1014 that the item is not waiting on the hold shelf, setting the
1015 priority to a non-zero value also sets the request's found
1016 status and waiting date to NULL.
1017
1018 If the hold is 'found' (waiting, in-transit, processing) the
1019 only field that can be updated is the expiration date.
1020
1021 The optional C<$itemnumber> parameter is used only when
1022 C<$rank> is a non-zero integer; if supplied, the itemnumber
1023 of the hold request is set accordingly; if omitted, the itemnumber
1024 is cleared.
1025
1026 B<FIXME:> Note that the forgoing can have the effect of causing
1027 item-level hold requests to turn into title-level requests.  This
1028 will be fixed once reserves has separate columns for requested
1029 itemnumber and supplying itemnumber.
1030
1031 =cut
1032
1033 sub ModReserve {
1034     my ( $params ) = @_;
1035
1036     my $rank = $params->{'rank'};
1037     my $reserve_id = $params->{'reserve_id'};
1038     my $branchcode = $params->{'branchcode'};
1039     my $itemnumber = $params->{'itemnumber'};
1040     my $suspend_until = $params->{'suspend_until'};
1041     my $borrowernumber = $params->{'borrowernumber'};
1042     my $biblionumber = $params->{'biblionumber'};
1043     my $cancellation_reason = $params->{'cancellation_reason'};
1044     my $date = $params->{expirationdate};
1045
1046     return if defined $rank && $rank eq "n";
1047
1048     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1049
1050     my $hold;
1051     unless ( $reserve_id ) {
1052         my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
1053         return unless $holds->count; # FIXME Should raise an exception
1054         $hold = $holds->next;
1055         $reserve_id = $hold->reserve_id;
1056     }
1057
1058     $hold ||= Koha::Holds->find($reserve_id);
1059
1060     # FIXME Other calls may fail
1061     Koha::Exceptions::ObjectNotFound->throw( 'No hold with id ' . $reserve_id ) unless $hold;
1062
1063     if ( $rank eq "del" ) {
1064         $hold->cancel({ cancellation_reason => $cancellation_reason });
1065     }
1066     elsif ($hold->found && $hold->priority eq '0' && $date) {
1067         logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
1068             if C4::Context->preference('HoldsLog');
1069
1070         # The only column that can be updated for a found hold is the expiration date
1071         $hold->expirationdate(dt_from_string($date))->store();
1072     }
1073     elsif ($rank =~ /^\d+/ and $rank > 0) {
1074         logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
1075             if C4::Context->preference('HoldsLog');
1076
1077         my $properties = {
1078             priority    => $rank,
1079             branchcode  => $branchcode,
1080             itemnumber  => $itemnumber,
1081             found       => undef,
1082             waitingdate => undef
1083         };
1084         if (exists $params->{reservedate}) {
1085             $properties->{reservedate} = $params->{reservedate} || undef;
1086         }
1087         if (exists $params->{expirationdate}) {
1088             $properties->{expirationdate} = $params->{expirationdate} || undef;
1089         }
1090
1091         $hold->set($properties)->store();
1092
1093         if ( defined( $suspend_until ) ) {
1094             if ( $suspend_until ) {
1095                 $suspend_until = eval { dt_from_string( $suspend_until ) };
1096                 $hold->suspend_hold( $suspend_until );
1097             } else {
1098                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1099                 # If the hold is not suspended, this does nothing.
1100                 $hold->set( { suspend_until => undef } )->store();
1101             }
1102         }
1103
1104         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1105     }
1106 }
1107
1108 =head2 ModReserveStatus
1109
1110   &ModReserveStatus($itemnumber, $newstatus);
1111
1112 Update the reserve status for the active (priority=0) reserve.
1113
1114 $itemnumber is the itemnumber the reserve is on
1115
1116 $newstatus is the new status.
1117
1118 =cut
1119
1120 sub ModReserveStatus {
1121
1122     #first : check if we have a reservation for this item .
1123     my ($itemnumber, $newstatus) = @_;
1124     my $dbh = C4::Context->dbh;
1125
1126     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1127     my $sth_set = $dbh->prepare($query);
1128     $sth_set->execute( $newstatus, $itemnumber );
1129
1130     my $item = Koha::Items->find($itemnumber);
1131     if ( $item->location && $item->location eq 'CART'
1132         && ( !$item->permanent_location || $item->permanent_location ne 'CART' )
1133         && $newstatus ) {
1134       CartToShelf( $itemnumber );
1135     }
1136 }
1137
1138 =head2 ModReserveAffect
1139
1140   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id, $desk_id);
1141
1142 This function affect an item and a status for a given reserve, either fetched directly
1143 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1144 is given, only first reserve returned is affected, which is ok for anything but
1145 multi-item holds.
1146
1147 if $transferToDo is not set, then the status is set to "Waiting" as well.
1148 otherwise, a transfer is on the way, and the end of the transfer will
1149 take care of the waiting status
1150
1151 This function also removes any entry of the hold in holds queue table.
1152
1153 =cut
1154
1155 sub ModReserveAffect {
1156     my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id, $desk_id ) = @_;
1157     my $dbh = C4::Context->dbh;
1158
1159     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1160     # attached to $itemnumber
1161     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1162     $sth->execute($itemnumber);
1163     my ($biblionumber) = $sth->fetchrow;
1164
1165     # get request - need to find out if item is already
1166     # waiting in order to not send duplicate hold filled notifications
1167
1168     my $hold;
1169     # Find hold by id if we have it
1170     $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1171     # Find item level hold for this item if there is one
1172     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1173     # Find record level hold if there is no item level hold
1174     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1175
1176     return unless $hold;
1177
1178     my $already_on_shelf = $hold->found && $hold->found eq 'W';
1179
1180     $hold->itemnumber($itemnumber);
1181
1182     if ($transferToDo) {
1183         $hold->set_transfer();
1184     } elsif (C4::Context->preference('HoldsNeedProcessingSIP')
1185              && C4::Context->interface eq 'sip'
1186              && !$already_on_shelf) {
1187         $hold->set_processing();
1188     } else {
1189         $hold->set_waiting($desk_id);
1190         _koha_notify_reserve( $hold->reserve_id ) unless $already_on_shelf;
1191         # Complete transfer if one exists
1192         my $transfer = $hold->item->get_transfer;
1193         $transfer->receive if $transfer;
1194     }
1195
1196     _FixPriority( { biblionumber => $biblionumber } );
1197     my $item = Koha::Items->find($itemnumber);
1198     if ( $item->location && $item->location eq 'CART'
1199         && ( !$item->permanent_location || $item->permanent_location ne 'CART' ) ) {
1200       CartToShelf( $itemnumber );
1201     }
1202
1203     my $std = $dbh->prepare(q{
1204         DELETE  q, t
1205         FROM    tmp_holdsqueue q
1206         INNER JOIN hold_fill_targets t
1207         ON  q.borrowernumber = t.borrowernumber
1208             AND q.biblionumber = t.biblionumber
1209             AND q.itemnumber = t.itemnumber
1210             AND q.item_level_request = t.item_level_request
1211             AND q.holdingbranch = t.source_branchcode
1212         WHERE t.reserve_id = ?
1213     });
1214     $std->execute($hold->reserve_id);
1215
1216     logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
1217         if C4::Context->preference('HoldsLog');
1218
1219     return;
1220 }
1221
1222 =head2 ModReserveCancelAll
1223
1224   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber,$reason);
1225
1226 function to cancel reserv,check other reserves, and transfer document if it's necessary
1227
1228 =cut
1229
1230 sub ModReserveCancelAll {
1231     my $messages;
1232     my $nextreservinfo;
1233     my ( $itemnumber, $borrowernumber, $cancellation_reason ) = @_;
1234
1235     #step 1 : cancel the reservation
1236     my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1237     return unless $holds->count;
1238     $holds->next->cancel({ cancellation_reason => $cancellation_reason });
1239
1240     #step 2 launch the subroutine of the others reserves
1241     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1242
1243     return ( $messages, $nextreservinfo->{borrowernumber} );
1244 }
1245
1246 =head2 ModReserveMinusPriority
1247
1248   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1249
1250 Reduce the values of queued list
1251
1252 =cut
1253
1254 sub ModReserveMinusPriority {
1255     my ( $itemnumber, $reserve_id ) = @_;
1256
1257     #first step update the value of the first person on reserv
1258     my $dbh   = C4::Context->dbh;
1259     my $query = "
1260         UPDATE reserves
1261         SET    priority = 0 , itemnumber = ?
1262         WHERE  reserve_id = ?
1263     ";
1264     my $sth_upd = $dbh->prepare($query);
1265     $sth_upd->execute( $itemnumber, $reserve_id );
1266     # second step update all others reserves
1267     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1268 }
1269
1270 =head2 IsAvailableForItemLevelRequest
1271
1272   my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1273
1274 Checks whether a given item record is available for an
1275 item-level hold request.  An item is available if
1276
1277 * it is not lost AND
1278 * it is not damaged AND
1279 * it is not withdrawn AND
1280 * a waiting or in transit reserve is placed on
1281 * does not have a not for loan value > 0
1282
1283 Need to check the issuingrules onshelfholds column,
1284 if this is set items on the shelf can be placed on hold
1285
1286 Note that IsAvailableForItemLevelRequest() does not
1287 check if the staff operator is authorized to place
1288 a request on the item - in particular,
1289 this routine does not check IndependentBranches
1290 and canreservefromotherbranches.
1291
1292 Note also that this subroutine does not checks smart
1293 rules limits for item by reservesallowed/holds_per_record
1294 values, this complemented in calling code with calls and
1295 checks with CanItemBeReserved or CanBookBeReserved.
1296
1297 =cut
1298
1299 sub IsAvailableForItemLevelRequest {
1300     my $item                = shift;
1301     my $patron              = shift;
1302     my $pickup_branchcode   = shift;
1303     # items_any_available is precalculated status passed from request.pl when set of items
1304     # looped outside of IsAvailableForItemLevelRequest to avoid nested loops:
1305     my $items_any_available = shift;
1306
1307     my $dbh = C4::Context->dbh;
1308     # must check the notforloan setting of the itemtype
1309     # FIXME - a lot of places in the code do this
1310     #         or something similar - need to be
1311     #         consolidated
1312     my $itemtype = $item->effective_itemtype;
1313     return 0
1314       unless defined $itemtype;
1315     my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1316
1317     return 0 if
1318         $notforloan_per_itemtype ||
1319         $item->itemlost        ||
1320         $item->notforloan > 0  || # item with negative or zero notforloan value is holdable
1321         $item->withdrawn        ||
1322         ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1323
1324     if ($pickup_branchcode) {
1325         my $destination = Koha::Libraries->find($pickup_branchcode);
1326         return 0 unless $destination;
1327         return 0 unless $destination->pickup_location;
1328         return 0 unless $item->can_be_transferred( { to => $destination } );
1329         my $reserves_control_branch =
1330             GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
1331         my $branchitemrule =
1332             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
1333         my $home_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
1334         return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
1335     }
1336
1337     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1338
1339     if ( $on_shelf_holds == 1 ) {
1340         return 1;
1341     } elsif ( $on_shelf_holds == 2 ) {
1342
1343         # if we have this param predefined from outer caller sub, we just need
1344         # to return it, so we saving from having loop inside other loop:
1345         return  $items_any_available ? 0 : 1
1346             if defined $items_any_available;
1347
1348         my $any_available = ItemsAnyAvailableAndNotRestricted( { biblionumber => $item->biblionumber, patron => $patron });
1349         return $any_available ? 0 : 1;
1350     } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1351         return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1352     }
1353 }
1354
1355 =head2 ItemsAnyAvailableAndNotRestricted
1356
1357   ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron });
1358
1359 This function checks all items for specified biblionumber (numeric) against patron (object)
1360 and returns true (1) if at least one item available for loan/check out/present/not held
1361 and also checks other parameters logic which not restricts item for hold at all (for ex.
1362 AllowHoldsOnDamagedItems or 'holdallowed' own/sibling library)
1363
1364 =cut
1365
1366 sub ItemsAnyAvailableAndNotRestricted {
1367     my $param = shift;
1368
1369     my @items = Koha::Items->search( { biblionumber => $param->{biblionumber} } )->as_list;
1370
1371     foreach my $i (@items) {
1372         my $reserves_control_branch =
1373             GetReservesControlBranch( $i->unblessed(), $param->{patron}->unblessed );
1374         my $branchitemrule =
1375             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1376         my $item_library = Koha::Libraries->find( { branchcode => $i->homebranch } );
1377
1378         # we can return (end the loop) when first one found:
1379         return 1
1380             unless $i->itemlost
1381             || $i->notforloan # items with non-zero notforloan cannot be checked out
1382             || $i->withdrawn
1383             || $i->onloan
1384             || IsItemOnHoldAndFound( $i->id )
1385             || ( $i->damaged
1386                  && ! C4::Context->preference('AllowHoldsOnDamagedItems') )
1387             || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1388             || $branchitemrule->{holdallowed} eq 'from_home_library' && $param->{patron}->branchcode ne $i->homebranch
1389             || $branchitemrule->{holdallowed} eq 'from_local_hold_group' && ! $item_library->validate_hold_sibling( { branchcode => $param->{patron}->branchcode } )
1390             || CanItemBeReserved( $param->{patron}, $i )->{status} ne 'OK';
1391     }
1392
1393     return 0;
1394 }
1395
1396 =head2 AlterPriority
1397
1398   AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1399
1400 This function changes a reserve's priority up, down, to the top, or to the bottom.
1401 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1402
1403 =cut
1404
1405 sub AlterPriority {
1406     my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1407
1408     my $hold = Koha::Holds->find( $reserve_id );
1409     return unless $hold;
1410
1411     if ( $hold->cancellationdate ) {
1412         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1413         return;
1414     }
1415
1416     if ( $where eq 'up' ) {
1417       return unless $prev_priority;
1418       _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1419     } elsif ( $where eq 'down' ) {
1420       return unless $next_priority;
1421       _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1422     } elsif ( $where eq 'top' ) {
1423       _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1424     } elsif ( $where eq 'bottom' ) {
1425       _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1426     }
1427
1428     # FIXME Should return the new priority
1429 }
1430
1431 =head2 ToggleLowestPriority
1432
1433   ToggleLowestPriority( $borrowernumber, $biblionumber );
1434
1435 This function sets the lowestPriority field to true if is false, and false if it is true.
1436
1437 =cut
1438
1439 sub ToggleLowestPriority {
1440     my ( $reserve_id ) = @_;
1441
1442     my $dbh = C4::Context->dbh;
1443
1444     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1445     $sth->execute( $reserve_id );
1446
1447     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1448 }
1449
1450 =head2 ToggleSuspend
1451
1452   ToggleSuspend( $reserve_id );
1453
1454 This function sets the suspend field to true if is false, and false if it is true.
1455 If the reserve is currently suspended with a suspend_until date, that date will
1456 be cleared when it is unsuspended.
1457
1458 =cut
1459
1460 sub ToggleSuspend {
1461     my ( $reserve_id, $suspend_until ) = @_;
1462
1463     $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1464
1465     my $hold = Koha::Holds->find( $reserve_id );
1466
1467     if ( $hold->is_suspended ) {
1468         $hold->resume()
1469     } else {
1470         $hold->suspend_hold( $suspend_until );
1471     }
1472 }
1473
1474 =head2 SuspendAll
1475
1476   SuspendAll(
1477       borrowernumber   => $borrowernumber,
1478       [ biblionumber   => $biblionumber, ]
1479       [ suspend_until  => $suspend_until, ]
1480       [ suspend        => $suspend ]
1481   );
1482
1483   This function accepts a set of hash keys as its parameters.
1484   It requires either borrowernumber or biblionumber, or both.
1485
1486   suspend_until is wholly optional.
1487
1488 =cut
1489
1490 sub SuspendAll {
1491     my %params = @_;
1492
1493     my $borrowernumber = $params{'borrowernumber'} || undef;
1494     my $biblionumber   = $params{'biblionumber'}   || undef;
1495     my $suspend_until  = $params{'suspend_until'}  || undef;
1496     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1497
1498     $suspend_until = eval { dt_from_string($suspend_until) }
1499       if ( defined($suspend_until) );
1500
1501     return unless ( $borrowernumber || $biblionumber );
1502
1503     my $params;
1504     $params->{found}          = undef;
1505     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1506     $params->{biblionumber}   = $biblionumber if $biblionumber;
1507
1508     my @holds = Koha::Holds->search($params)->as_list;
1509
1510     if ($suspend) {
1511         map { $_->suspend_hold($suspend_until) } @holds;
1512     }
1513     else {
1514         map { $_->resume() } @holds;
1515     }
1516 }
1517
1518
1519 =head2 _FixPriority
1520
1521   _FixPriority({
1522     reserve_id => $reserve_id,
1523     [rank => $rank,]
1524     [ignoreSetLowestRank => $ignoreSetLowestRank]
1525   });
1526
1527   or
1528
1529   _FixPriority({ biblionumber => $biblionumber});
1530
1531 This routine adjusts the priority of a hold request and holds
1532 on the same bib.
1533
1534 In the first form, where a reserve_id is passed, the priority of the
1535 hold is set to supplied rank, and other holds for that bib are adjusted
1536 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1537 is supplied, all of the holds on that bib have their priority adjusted
1538 as if the second form had been used.
1539
1540 In the second form, where a biblionumber is passed, the holds on that
1541 bib (that are not captured) are sorted in order of increasing priority,
1542 then have reserves.priority set so that the first non-captured hold
1543 has its priority set to 1, the second non-captured hold has its priority
1544 set to 2, and so forth.
1545
1546 In both cases, holds that have the lowestPriority flag on are have their
1547 priority adjusted to ensure that they remain at the end of the line.
1548
1549 Note that the ignoreSetLowestRank parameter is meant to be used only
1550 when _FixPriority calls itself.
1551
1552 =cut
1553
1554 sub _FixPriority {
1555     my ( $params ) = @_;
1556     my $reserve_id = $params->{reserve_id};
1557     my $rank = $params->{rank} // '';
1558     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1559     my $biblionumber = $params->{biblionumber};
1560
1561     my $dbh = C4::Context->dbh;
1562
1563     my $hold;
1564     if ( $reserve_id ) {
1565         $hold = Koha::Holds->find( $reserve_id );
1566         if (!defined $hold){
1567             # may have already been checked out and hold fulfilled
1568             $hold = Koha::Old::Holds->find( $reserve_id );
1569         }
1570         return unless $hold;
1571     }
1572
1573     unless ( $biblionumber ) { # FIXME This is a very weird API
1574         $biblionumber = $hold->biblionumber;
1575     }
1576
1577     if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1578         $hold->cancel;
1579     }
1580     elsif ( $reserve_id && ( $rank eq "W" || $rank eq "0" ) ) {
1581
1582         # make sure priority for waiting or in-transit items is 0
1583         my $query = "
1584             UPDATE reserves
1585             SET    priority = 0
1586             WHERE reserve_id = ?
1587             AND found IN ('W', 'T', 'P')
1588         ";
1589         my $sth = $dbh->prepare($query);
1590         $sth->execute( $reserve_id );
1591     }
1592     my @priority;
1593
1594     # get whats left
1595     my $query = "
1596         SELECT reserve_id, borrowernumber, reservedate
1597         FROM   reserves
1598         WHERE  biblionumber   = ?
1599           AND  ((found <> 'W' AND found <> 'T' AND found <> 'P') OR found IS NULL)
1600         ORDER BY priority ASC
1601     ";
1602     my $sth = $dbh->prepare($query);
1603     $sth->execute( $biblionumber );
1604     while ( my $line = $sth->fetchrow_hashref ) {
1605         push( @priority,     $line );
1606     }
1607
1608     # FIXME This whole sub must be rewritten, especially to highlight what is done when reserve_id is not given
1609     # To find the matching index
1610     my $i;
1611     my $key = -1;    # to allow for 0 to be a valid result
1612     for ( $i = 0 ; $i < @priority ; $i++ ) {
1613         if ( $reserve_id && $reserve_id == $priority[$i]->{'reserve_id'} ) {
1614             $key = $i;    # save the index
1615             last;
1616         }
1617     }
1618
1619     # if index exists in array then move it to new position
1620     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1621         my $new_rank = $rank - 1; # $new_rank is what you want the new index to be in the array
1622         my $moving_item = splice( @priority, $key, 1 );
1623         $new_rank = scalar @priority if $new_rank > scalar @priority;
1624         splice( @priority, $new_rank, 0, $moving_item );
1625     }
1626
1627     # now fix the priority on those that are left....
1628     $query = "
1629         UPDATE reserves
1630         SET    priority = ?
1631         WHERE  reserve_id = ?
1632     ";
1633     $sth = $dbh->prepare($query);
1634     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1635         $sth->execute(
1636             $j + 1,
1637             $priority[$j]->{'reserve_id'}
1638         );
1639     }
1640
1641     unless ( $ignoreSetLowestRank ) {
1642         $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 AND biblionumber = ? ORDER BY priority" );
1643         $sth->execute($biblionumber);
1644       while ( my $res = $sth->fetchrow_hashref() ) {
1645         _FixPriority({
1646             reserve_id => $res->{'reserve_id'},
1647             rank => '999999',
1648             ignoreSetLowestRank => 1
1649         });
1650       }
1651     }
1652 }
1653
1654 =head2 _Findgroupreserve
1655
1656   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1657
1658 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1659 first match found.  If neither, then we look for non-holds-queue based holds.
1660 Lookahead is the number of days to look in advance.
1661
1662 C<&_Findgroupreserve> returns :
1663 C<@results> is an array of references-to-hash whose keys are mostly
1664 fields from the reserves table of the Koha database, plus
1665 C<biblioitemnumber>.
1666
1667 This routine with either return:
1668 1 - Item specific holds from the holds queue
1669 2 - Title level holds from the holds queue
1670 3 - All holds for this biblionumber
1671
1672 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1673
1674 =cut
1675
1676 sub _Findgroupreserve {
1677     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1678     my $dbh   = C4::Context->dbh;
1679
1680     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1681     # check for exact targeted match
1682     my $item_level_target_query = qq{
1683         SELECT reserves.biblionumber        AS biblionumber,
1684                reserves.borrowernumber      AS borrowernumber,
1685                reserves.reservedate         AS reservedate,
1686                reserves.branchcode          AS branchcode,
1687                reserves.cancellationdate    AS cancellationdate,
1688                reserves.found               AS found,
1689                reserves.reservenotes        AS reservenotes,
1690                reserves.priority            AS priority,
1691                reserves.timestamp           AS timestamp,
1692                biblioitems.biblioitemnumber AS biblioitemnumber,
1693                reserves.itemnumber          AS itemnumber,
1694                reserves.reserve_id          AS reserve_id,
1695                reserves.itemtype            AS itemtype,
1696                reserves.non_priority        AS non_priority
1697         FROM reserves
1698         JOIN biblioitems USING (biblionumber)
1699         JOIN hold_fill_targets USING (reserve_id)
1700         WHERE found IS NULL
1701         AND priority > 0
1702         AND item_level_request = 1
1703         AND hold_fill_targets.itemnumber = ?
1704         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1705         AND suspend = 0
1706         ORDER BY priority
1707     };
1708     my $sth = $dbh->prepare($item_level_target_query);
1709     $sth->execute($itemnumber, $lookahead||0);
1710     my @results;
1711     if ( my $data = $sth->fetchrow_hashref ) {
1712         push( @results, $data )
1713           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1714     }
1715     return @results if @results;
1716
1717     # check for title-level targeted match
1718     my $title_level_target_query = qq{
1719         SELECT reserves.biblionumber        AS biblionumber,
1720                reserves.borrowernumber      AS borrowernumber,
1721                reserves.reservedate         AS reservedate,
1722                reserves.branchcode          AS branchcode,
1723                reserves.cancellationdate    AS cancellationdate,
1724                reserves.found               AS found,
1725                reserves.reservenotes        AS reservenotes,
1726                reserves.priority            AS priority,
1727                reserves.timestamp           AS timestamp,
1728                biblioitems.biblioitemnumber AS biblioitemnumber,
1729                reserves.itemnumber          AS itemnumber,
1730                reserves.reserve_id          AS reserve_id,
1731                reserves.itemtype            AS itemtype,
1732                reserves.non_priority        AS non_priority
1733         FROM reserves
1734         JOIN biblioitems USING (biblionumber)
1735         JOIN hold_fill_targets USING (reserve_id)
1736         WHERE found IS NULL
1737         AND priority > 0
1738         AND item_level_request = 0
1739         AND hold_fill_targets.itemnumber = ?
1740         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1741         AND suspend = 0
1742         ORDER BY priority
1743     };
1744     $sth = $dbh->prepare($title_level_target_query);
1745     $sth->execute($itemnumber, $lookahead||0);
1746     @results = ();
1747     if ( my $data = $sth->fetchrow_hashref ) {
1748         push( @results, $data )
1749           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1750     }
1751     return @results if @results;
1752
1753     my $query = qq{
1754         SELECT reserves.biblionumber               AS biblionumber,
1755                reserves.borrowernumber             AS borrowernumber,
1756                reserves.reservedate                AS reservedate,
1757                reserves.waitingdate                AS waitingdate,
1758                reserves.branchcode                 AS branchcode,
1759                reserves.cancellationdate           AS cancellationdate,
1760                reserves.found                      AS found,
1761                reserves.reservenotes               AS reservenotes,
1762                reserves.priority                   AS priority,
1763                reserves.timestamp                  AS timestamp,
1764                reserves.itemnumber                 AS itemnumber,
1765                reserves.reserve_id                 AS reserve_id,
1766                reserves.itemtype                   AS itemtype,
1767                reserves.non_priority        AS non_priority
1768         FROM reserves
1769         WHERE reserves.biblionumber = ?
1770           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1771           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1772           AND suspend = 0
1773           ORDER BY priority
1774     };
1775     $sth = $dbh->prepare($query);
1776     $sth->execute( $biblio, $itemnumber, $lookahead||0);
1777     @results = ();
1778     while ( my $data = $sth->fetchrow_hashref ) {
1779         push( @results, $data )
1780           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1781     }
1782     return @results;
1783 }
1784
1785 =head2 _koha_notify_reserve
1786
1787   _koha_notify_reserve( $hold->reserve_id );
1788
1789 Sends a notification to the patron that their hold has been filled (through
1790 ModReserveAffect)
1791
1792 The letter code for this notice may be found using the following query:
1793
1794     select distinct letter_code
1795     from message_transports
1796     inner join message_attributes using (message_attribute_id)
1797     where message_name = 'Hold_Filled'
1798
1799 This will probably sipmly be 'HOLD', but because it is defined in the database,
1800 it is subject to addition or change.
1801
1802 The following tables are availalbe witin the notice:
1803
1804     branches
1805     borrowers
1806     biblio
1807     biblioitems
1808     reserves
1809     items
1810
1811 =cut
1812
1813 sub _koha_notify_reserve {
1814     my $reserve_id = shift;
1815     my $hold = Koha::Holds->find($reserve_id);
1816     my $borrowernumber = $hold->borrowernumber;
1817
1818     my $patron = Koha::Patrons->find( $borrowernumber );
1819
1820     # Try to get the borrower's email address
1821     my $to_address = $patron->notice_email_address;
1822
1823     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1824             borrowernumber => $borrowernumber,
1825             message_name => 'Hold_Filled'
1826     } );
1827
1828     my $library = Koha::Libraries->find( $hold->branchcode );
1829     my $admin_email_address = $library->from_email_address;
1830     $library = $library->unblessed;
1831
1832     my %letter_params = (
1833         module => 'reserves',
1834         branchcode => $hold->branchcode,
1835         lang => $patron->lang,
1836         tables => {
1837             'branches'       => $library,
1838             'borrowers'      => $patron->unblessed,
1839             'biblio'         => $hold->biblionumber,
1840             'biblioitems'    => $hold->biblionumber,
1841             'reserves'       => $hold->unblessed,
1842             'items'          => $hold->itemnumber,
1843         },
1844     );
1845
1846     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.
1847     my $send_notification = sub {
1848         my ( $mtt, $letter_code ) = (@_);
1849         return unless defined $letter_code;
1850         $letter_params{letter_code} = $letter_code;
1851         $letter_params{message_transport_type} = $mtt;
1852         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1853         unless ($letter) {
1854             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1855             return;
1856         }
1857
1858         C4::Letters::EnqueueLetter( {
1859             letter => $letter,
1860             borrowernumber => $borrowernumber,
1861             from_address => $admin_email_address,
1862             message_transport_type => $mtt,
1863         } );
1864     };
1865
1866     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1867         next if (
1868                ( $mtt eq 'email' and not $to_address ) # No email address
1869             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
1870             or ( $mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1871             or ( $mtt eq 'phone' and not $patron->phone ) # No phone number to call
1872         );
1873
1874         &$send_notification($mtt, $letter_code);
1875         $notification_sent++;
1876     }
1877     #Making sure that a print notification is sent if no other transport types can be utilized.
1878     if (! $notification_sent) {
1879         &$send_notification('print', 'HOLD');
1880     }
1881
1882 }
1883
1884 =head2 _ShiftPriorityByDateAndPriority
1885
1886   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1887
1888 This increments the priority of all reserves after the one
1889 with either the lowest date after C<$reservedate>
1890 or the lowest priority after C<$priority>.
1891
1892 It effectively makes room for a new reserve to be inserted with a certain
1893 priority, which is returned.
1894
1895 This is most useful when the reservedate can be set by the user.  It allows
1896 the new reserve to be placed before other reserves that have a later
1897 reservedate.  Since priority also is set by the form in reserves/request.pl
1898 the sub accounts for that too.
1899
1900 =cut
1901
1902 sub _ShiftPriorityByDateAndPriority {
1903     my ( $biblio, $resdate, $new_priority ) = @_;
1904
1905     my $dbh = C4::Context->dbh;
1906     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1907     my $sth = $dbh->prepare( $query );
1908     $sth->execute( $biblio, $resdate, $new_priority );
1909     my $min_priority = $sth->fetchrow;
1910     # if no such matches are found, $new_priority remains as original value
1911     $new_priority = $min_priority if ( $min_priority );
1912
1913     # Shift the priority up by one; works in conjunction with the next SQL statement
1914     $query = "UPDATE reserves
1915               SET priority = priority+1
1916               WHERE biblionumber = ?
1917               AND borrowernumber = ?
1918               AND reservedate = ?
1919               AND found IS NULL";
1920     my $sth_update = $dbh->prepare( $query );
1921
1922     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1923     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1924     $sth = $dbh->prepare( $query );
1925     $sth->execute( $new_priority, $biblio );
1926     while ( my $row = $sth->fetchrow_hashref ) {
1927         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1928     }
1929
1930     return $new_priority;  # so the caller knows what priority they wind up receiving
1931 }
1932
1933 =head2 MoveReserve
1934
1935   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1936
1937 Use when checking out an item to handle reserves
1938 If $cancelreserve boolean is set to true, it will remove existing reserve
1939
1940 =cut
1941
1942 sub MoveReserve {
1943     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1944
1945     $cancelreserve //= 0;
1946
1947     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1948     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1949     return unless $res;
1950
1951     my $biblionumber = $res->{biblionumber};
1952
1953     if ($res->{borrowernumber} == $borrowernumber) {
1954         my $hold = Koha::Holds->find( $res->{reserve_id} );
1955         $hold->fill;
1956     }
1957     else {
1958         # warn "Reserved";
1959         # The item is reserved by someone else.
1960         # Find this item in the reserves
1961
1962         my $borr_res  = Koha::Holds->search({
1963             borrowernumber => $borrowernumber,
1964             biblionumber   => $biblionumber,
1965         },{
1966             order_by       => 'priority'
1967         })->next();
1968
1969         if ( $borr_res ) {
1970             # The item is reserved by the current patron
1971             $borr_res->fill;
1972         }
1973
1974         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1975             RevertWaitingStatus({ itemnumber => $itemnumber });
1976         }
1977         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1978             my $hold = Koha::Holds->find( $res->{reserve_id} );
1979             $hold->cancel;
1980         }
1981     }
1982 }
1983
1984 =head2 MergeHolds
1985
1986   MergeHolds($dbh,$to_biblio, $from_biblio);
1987
1988 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1989
1990 =cut
1991
1992 sub MergeHolds {
1993     my ( $dbh, $to_biblio, $from_biblio ) = @_;
1994     my $sth = $dbh->prepare(
1995         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1996     );
1997     $sth->execute($from_biblio);
1998     if ( my $data = $sth->fetchrow_hashref() ) {
1999
2000         # holds exist on old record, if not we don't need to do anything
2001         $sth = $dbh->prepare(
2002             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2003         $sth->execute( $to_biblio, $from_biblio );
2004
2005         # Reorder by date
2006         # don't reorder those already waiting
2007
2008         $sth = $dbh->prepare(
2009 "SELECT * FROM reserves WHERE biblionumber = ? AND (found NOT IN ('W', 'T', 'P') OR found is NULL) ORDER BY reservedate ASC"
2010         );
2011         my $upd_sth = $dbh->prepare(
2012 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2013         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
2014         );
2015         $sth->execute( $to_biblio );
2016         my $priority = 1;
2017         while ( my $reserve = $sth->fetchrow_hashref() ) {
2018             $upd_sth->execute(
2019                 $priority,                    $to_biblio,
2020                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2021                 $reserve->{'itemnumber'}
2022             );
2023             $priority++;
2024         }
2025     }
2026 }
2027
2028 =head2 RevertWaitingStatus
2029
2030   RevertWaitingStatus({ itemnumber => $itemnumber });
2031
2032   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2033
2034   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2035           item level hold, even if it was only a bibliolevel hold to
2036           begin with. This is because we can no longer know if a hold
2037           was item-level or bib-level after a hold has been set to
2038           waiting status.
2039
2040 =cut
2041
2042 sub RevertWaitingStatus {
2043     my ( $params ) = @_;
2044     my $itemnumber = $params->{'itemnumber'};
2045
2046     return unless ( $itemnumber );
2047
2048     my $dbh = C4::Context->dbh;
2049
2050     ## Get the waiting reserve we want to revert
2051     my $hold = Koha::Holds->search(
2052         {
2053             itemnumber => $itemnumber,
2054             found => { not => undef },
2055         }
2056     )->next;
2057
2058     ## Increment the priority of all other non-waiting
2059     ## reserves for this bib record
2060     my $holds = Koha::Holds->search({ biblionumber => $hold->biblionumber, priority => { '>' => 0 } })
2061                            ->update({ priority => \'priority + 1' }, { no_triggers => 1 });
2062
2063     ## Fix up the currently waiting reserve
2064     $hold->set(
2065         {
2066             priority    => 1,
2067             found       => undef,
2068             waitingdate => undef,
2069             expirationdate => $hold->patron_expiration_date,
2070             itemnumber  => $hold->item_level_hold ? $hold->itemnumber : undef,
2071         }
2072     )->store();
2073
2074     _FixPriority( { biblionumber => $hold->biblionumber } );
2075
2076     return $hold;
2077 }
2078
2079 =head2 ReserveSlip
2080
2081 ReserveSlip(
2082     {
2083         branchcode     => $branchcode,
2084         borrowernumber => $borrowernumber,
2085         biblionumber   => $biblionumber,
2086         [ itemnumber   => $itemnumber, ]
2087         [ barcode      => $barcode, ]
2088     }
2089   )
2090
2091 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2092
2093 The letter code will be HOLD_SLIP, and the following tables are
2094 available within the slip:
2095
2096     reserves
2097     branches
2098     borrowers
2099     biblio
2100     biblioitems
2101     items
2102
2103 =cut
2104
2105 sub ReserveSlip {
2106     my ($args) = @_;
2107     my $branchcode     = $args->{branchcode};
2108     my $reserve_id = $args->{reserve_id};
2109
2110     my $hold = Koha::Holds->find($reserve_id);
2111     return unless $hold;
2112
2113     my $patron = $hold->borrower;
2114     my $reserve = $hold->unblessed;
2115
2116     return  C4::Letters::GetPreparedLetter (
2117         module => 'circulation',
2118         letter_code => 'HOLD_SLIP',
2119         branchcode => $branchcode,
2120         lang => $patron->lang,
2121         tables => {
2122             'reserves'    => $reserve,
2123             'branches'    => $reserve->{branchcode},
2124             'borrowers'   => $reserve->{borrowernumber},
2125             'biblio'      => $reserve->{biblionumber},
2126             'biblioitems' => $reserve->{biblionumber},
2127             'items'       => $reserve->{itemnumber},
2128         },
2129     );
2130 }
2131
2132 =head2 GetReservesControlBranch
2133
2134   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2135
2136   Return the branchcode to be used to determine which reserves
2137   policy applies to a transaction.
2138
2139   C<$item> is a hashref for an item. Only 'homebranch' is used.
2140
2141   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2142
2143 =cut
2144
2145 sub GetReservesControlBranch {
2146     my ( $item, $borrower ) = @_;
2147
2148     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2149
2150     my $branchcode =
2151         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2152       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2153       :                                              undef;
2154
2155     return $branchcode;
2156 }
2157
2158 =head2 CalculatePriority
2159
2160     my $p = CalculatePriority($biblionumber, $resdate);
2161
2162 Calculate priority for a new reserve on biblionumber, placing it at
2163 the end of the line of all holds whose start date falls before
2164 the current system time and that are neither on the hold shelf
2165 or in transit.
2166
2167 The reserve date parameter is optional; if it is supplied, the
2168 priority is based on the set of holds whose start date falls before
2169 the parameter value.
2170
2171 After calculation of this priority, it is recommended to call
2172 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2173 AddReserves.
2174
2175 =cut
2176
2177 sub CalculatePriority {
2178     my ( $biblionumber, $resdate ) = @_;
2179
2180     my $sql = q{
2181         SELECT COUNT(*) FROM reserves
2182         WHERE biblionumber = ?
2183         AND   priority > 0
2184         AND   (found IS NULL OR found = '')
2185     };
2186     #skip found==W or found==T or found==P (waiting, transit or processing holds)
2187     if( $resdate ) {
2188         $sql.= ' AND ( reservedate <= ? )';
2189     }
2190     else {
2191         $sql.= ' AND ( reservedate < NOW() )';
2192     }
2193     my $dbh = C4::Context->dbh();
2194     my @row = $dbh->selectrow_array(
2195         $sql,
2196         undef,
2197         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2198     );
2199
2200     return @row ? $row[0]+1 : 1;
2201 }
2202
2203 =head2 IsItemOnHoldAndFound
2204
2205     my $bool = IsItemFoundHold( $itemnumber );
2206
2207     Returns true if the item is currently on hold
2208     and that hold has a non-null found status ( W, T, etc. )
2209
2210 =cut
2211
2212 sub IsItemOnHoldAndFound {
2213     my ($itemnumber) = @_;
2214
2215     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2216
2217     my $found = $rs->count(
2218         {
2219             itemnumber => $itemnumber,
2220             found      => { '!=' => undef }
2221         }
2222     );
2223
2224     return $found;
2225 }
2226
2227 =head2 GetMaxPatronHoldsForRecord
2228
2229 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2230
2231 For multiple holds on a given record for a given patron, the max
2232 number of record level holds that a patron can be placed is the highest
2233 value of the holds_per_record rule for each item if the record for that
2234 patron. This subroutine finds and returns the highest holds_per_record
2235 rule value for a given patron id and record id.
2236
2237 =cut
2238
2239 sub GetMaxPatronHoldsForRecord {
2240     my ( $borrowernumber, $biblionumber ) = @_;
2241
2242     my $patron = Koha::Patrons->find($borrowernumber);
2243     my @items = Koha::Items->search( { biblionumber => $biblionumber } )->as_list;
2244
2245     my $controlbranch = C4::Context->preference('ReservesControlBranch');
2246
2247     my $categorycode = $patron->categorycode;
2248     my $branchcode;
2249     $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2250
2251     my $max = 0;
2252     foreach my $item (@items) {
2253         my $itemtype = $item->effective_itemtype();
2254
2255         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2256
2257         my $rule = Koha::CirculationRules->get_effective_rule({
2258             categorycode => $categorycode,
2259             itemtype     => $itemtype,
2260             branchcode   => $branchcode,
2261             rule_name    => 'holds_per_record'
2262         });
2263         my $holds_per_record = $rule ? $rule->rule_value : 0;
2264         $max = $holds_per_record if $holds_per_record > $max;
2265     }
2266
2267     return $max;
2268 }
2269
2270 =head1 AUTHOR
2271
2272 Koha Development Team <http://koha-community.org/>
2273
2274 =cut
2275
2276 1;