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