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