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