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