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