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