Bug 26262: Remove wrong dom setting in course reserves (OPAC)
[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                     unless ($item->exclude_from_local_holds_priority || $patron->category->exclude_from_local_holds_priority) {
847                         my $local_holds_priority_item_branchcode =
848                             $item->$LocalHoldsPriorityItemControl;
849                         my $local_holds_priority_patron_branchcode =
850                             ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
851                             ? $res->{branchcode}
852                             : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
853                             ? $patron->branchcode
854                             : undef;
855                         $local_hold_match =
856                             $local_holds_priority_item_branchcode eq
857                             $local_holds_priority_patron_branchcode;
858                     }
859                 }
860
861                 # See if this item is more important than what we've got so far
862                 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
863                     $item ||= Koha::Items->find($itemnumber);
864                     next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
865                     $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
866                     my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
867                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
868                     next if ($branchitemrule->{'holdallowed'} == 0);
869                     next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
870                     my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
871                     next if (($branchitemrule->{'holdallowed'} == 3) && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
872                     my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
873                     next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
874                     next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
875                     next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
876                     next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
877                     $priority = $res->{'priority'};
878                     $highest  = $res;
879                     last if $local_hold_match;
880                 }
881             }
882         }
883     }
884
885     # If we get this far, then no exact match was found.
886     # We return the most important (i.e. next) reservation.
887     if ($highest) {
888         $highest->{'itemnumber'} = $item;
889         return ( "Reserved", $highest, \@reserves );
890     }
891
892     return ( '' );
893 }
894
895 =head2 CancelExpiredReserves
896
897   CancelExpiredReserves();
898
899 Cancels all reserves with an expiration date from before today.
900
901 =cut
902
903 sub CancelExpiredReserves {
904     my $today = dt_from_string();
905     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
906     my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
907
908     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
909     my $params = { expirationdate => { '<', $dtf->format_date($today) } };
910     $params->{found} = [ { '!=', 'W' }, undef ]  unless $expireWaiting;
911
912     # FIXME To move to Koha::Holds->search_expired (?)
913     my $holds = Koha::Holds->search( $params );
914
915     while ( my $hold = $holds->next ) {
916         my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
917
918         next if !$cancel_on_holidays && $calendar->is_holiday( $today );
919
920         my $cancel_params = {};
921         if ( $hold->found eq 'W' ) {
922             $cancel_params->{charge_cancel_fee} = 1;
923         }
924         $hold->cancel( $cancel_params );
925     }
926 }
927
928 =head2 AutoUnsuspendReserves
929
930   AutoUnsuspendReserves();
931
932 Unsuspends all suspended reserves with a suspend_until date from before today.
933
934 =cut
935
936 sub AutoUnsuspendReserves {
937     my $today = dt_from_string();
938
939     my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
940
941     map { $_->resume() } @holds;
942 }
943
944 =head2 ModReserve
945
946   ModReserve({ rank => $rank,
947                reserve_id => $reserve_id,
948                branchcode => $branchcode
949                [, itemnumber => $itemnumber ]
950                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
951               });
952
953 Change a hold request's priority or cancel it.
954
955 C<$rank> specifies the effect of the change.  If C<$rank>
956 is 'W' or 'n', nothing happens.  This corresponds to leaving a
957 request alone when changing its priority in the holds queue
958 for a bib.
959
960 If C<$rank> is 'del', the hold request is cancelled.
961
962 If C<$rank> is an integer greater than zero, the priority of
963 the request is set to that value.  Since priority != 0 means
964 that the item is not waiting on the hold shelf, setting the
965 priority to a non-zero value also sets the request's found
966 status and waiting date to NULL.
967
968 The optional C<$itemnumber> parameter is used only when
969 C<$rank> is a non-zero integer; if supplied, the itemnumber
970 of the hold request is set accordingly; if omitted, the itemnumber
971 is cleared.
972
973 B<FIXME:> Note that the forgoing can have the effect of causing
974 item-level hold requests to turn into title-level requests.  This
975 will be fixed once reserves has separate columns for requested
976 itemnumber and supplying itemnumber.
977
978 =cut
979
980 sub ModReserve {
981     my ( $params ) = @_;
982
983     my $rank = $params->{'rank'};
984     my $reserve_id = $params->{'reserve_id'};
985     my $branchcode = $params->{'branchcode'};
986     my $itemnumber = $params->{'itemnumber'};
987     my $suspend_until = $params->{'suspend_until'};
988     my $borrowernumber = $params->{'borrowernumber'};
989     my $biblionumber = $params->{'biblionumber'};
990     my $cancellation_reason = $params->{'cancellation_reason'};
991
992     return if $rank eq "W";
993     return if $rank eq "n";
994
995     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
996
997     my $hold;
998     unless ( $reserve_id ) {
999         my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
1000         return unless $holds->count; # FIXME Should raise an exception
1001         $hold = $holds->next;
1002         $reserve_id = $hold->reserve_id;
1003     }
1004
1005     $hold ||= Koha::Holds->find($reserve_id);
1006
1007     if ( $rank eq "del" ) {
1008         $hold->cancel({ cancellation_reason => $cancellation_reason });
1009     }
1010     elsif ($rank =~ /^\d+/ and $rank > 0) {
1011         logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1012             if C4::Context->preference('HoldsLog');
1013
1014         my $properties = {
1015             priority    => $rank,
1016             branchcode  => $branchcode,
1017             itemnumber  => $itemnumber,
1018             found       => undef,
1019             waitingdate => undef
1020         };
1021         if (exists $params->{reservedate}) {
1022             $properties->{reservedate} = $params->{reservedate} || undef;
1023         }
1024         if (exists $params->{expirationdate}) {
1025             $properties->{expirationdate} = $params->{expirationdate} || undef;
1026         }
1027
1028         $hold->set($properties)->store();
1029
1030         if ( defined( $suspend_until ) ) {
1031             if ( $suspend_until ) {
1032                 $suspend_until = eval { dt_from_string( $suspend_until ) };
1033                 $hold->suspend_hold( $suspend_until );
1034             } else {
1035                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1036                 # If the hold is not suspended, this does nothing.
1037                 $hold->set( { suspend_until => undef } )->store();
1038             }
1039         }
1040
1041         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1042     }
1043 }
1044
1045 =head2 ModReserveFill
1046
1047   &ModReserveFill($reserve);
1048
1049 Fill a reserve. If I understand this correctly, this means that the
1050 reserved book has been found and given to the patron who reserved it.
1051
1052 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1053 whose keys are fields from the reserves table in the Koha database.
1054
1055 =cut
1056
1057 sub ModReserveFill {
1058     my ($res) = @_;
1059     my $reserve_id = $res->{'reserve_id'};
1060
1061     my $hold = Koha::Holds->find($reserve_id);
1062     # get the priority on this record....
1063     my $priority = $hold->priority;
1064
1065     # update the hold statuses, no need to store it though, we will be deleting it anyway
1066     $hold->set(
1067         {
1068             found    => 'F',
1069             priority => 0,
1070         }
1071     );
1072
1073     logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1074         if C4::Context->preference('HoldsLog');
1075
1076     # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
1077     Koha::Old::Hold->new( $hold->unblessed() )->store();
1078
1079     $hold->delete();
1080
1081     if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1082         my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1083         ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1084     }
1085
1086     # now fix the priority on the others (if the priority wasn't
1087     # already sorted!)....
1088     unless ( $priority == 0 ) {
1089         _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1090     }
1091 }
1092
1093 =head2 ModReserveStatus
1094
1095   &ModReserveStatus($itemnumber, $newstatus);
1096
1097 Update the reserve status for the active (priority=0) reserve.
1098
1099 $itemnumber is the itemnumber the reserve is on
1100
1101 $newstatus is the new status.
1102
1103 =cut
1104
1105 sub ModReserveStatus {
1106
1107     #first : check if we have a reservation for this item .
1108     my ($itemnumber, $newstatus) = @_;
1109     my $dbh = C4::Context->dbh;
1110
1111     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1112     my $sth_set = $dbh->prepare($query);
1113     $sth_set->execute( $newstatus, $itemnumber );
1114
1115     my $item = Koha::Items->find($itemnumber);
1116     if ( $item->location && $item->location eq 'CART'
1117         && ( !$item->permanent_location || $item->permanent_location ne 'CART' )
1118         && $newstatus ) {
1119       CartToShelf( $itemnumber );
1120     }
1121 }
1122
1123 =head2 ModReserveAffect
1124
1125   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1126
1127 This function affect an item and a status for a given reserve, either fetched directly
1128 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1129 is given, only first reserve returned is affected, which is ok for anything but
1130 multi-item holds.
1131
1132 if $transferToDo is not set, then the status is set to "Waiting" as well.
1133 otherwise, a transfer is on the way, and the end of the transfer will
1134 take care of the waiting status
1135
1136 =cut
1137
1138 sub ModReserveAffect {
1139     my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1140     my $dbh = C4::Context->dbh;
1141
1142     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1143     # attached to $itemnumber
1144     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1145     $sth->execute($itemnumber);
1146     my ($biblionumber) = $sth->fetchrow;
1147
1148     # get request - need to find out if item is already
1149     # waiting in order to not send duplicate hold filled notifications
1150
1151     my $hold;
1152     # Find hold by id if we have it
1153     $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1154     # Find item level hold for this item if there is one
1155     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1156     # Find record level hold if there is no item level hold
1157     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1158
1159     return unless $hold;
1160
1161     my $already_on_shelf = $hold->found && $hold->found eq 'W';
1162
1163     $hold->itemnumber($itemnumber);
1164     $hold->set_waiting($transferToDo);
1165
1166     if( !$transferToDo ){
1167         _koha_notify_reserve( $hold->reserve_id ) unless $already_on_shelf;
1168         my $transfers = Koha::Item::Transfers->search({
1169             itemnumber => $itemnumber,
1170             datearrived => undef
1171         });
1172         while( my $transfer = $transfers->next ){
1173             $transfer->datearrived( dt_from_string() )->store;
1174         };
1175     }
1176
1177
1178     _FixPriority( { biblionumber => $biblionumber } );
1179     my $item = Koha::Items->find($itemnumber);
1180     if ( $item->location && $item->location eq 'CART'
1181         && ( !$item->permanent_location || $item->permanent_location ne 'CART' ) ) {
1182       CartToShelf( $itemnumber );
1183     }
1184
1185     logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1186         if C4::Context->preference('HoldsLog');
1187
1188     return;
1189 }
1190
1191 =head2 ModReserveCancelAll
1192
1193   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber,$reason);
1194
1195 function to cancel reserv,check other reserves, and transfer document if it's necessary
1196
1197 =cut
1198
1199 sub ModReserveCancelAll {
1200     my $messages;
1201     my $nextreservinfo;
1202     my ( $itemnumber, $borrowernumber, $cancellation_reason ) = @_;
1203
1204     #step 1 : cancel the reservation
1205     my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1206     return unless $holds->count;
1207     $holds->next->cancel({ cancellation_reason => $cancellation_reason });
1208
1209     #step 2 launch the subroutine of the others reserves
1210     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1211
1212     return ( $messages, $nextreservinfo->{borrowernumber} );
1213 }
1214
1215 =head2 ModReserveMinusPriority
1216
1217   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1218
1219 Reduce the values of queued list
1220
1221 =cut
1222
1223 sub ModReserveMinusPriority {
1224     my ( $itemnumber, $reserve_id ) = @_;
1225
1226     #first step update the value of the first person on reserv
1227     my $dbh   = C4::Context->dbh;
1228     my $query = "
1229         UPDATE reserves
1230         SET    priority = 0 , itemnumber = ?
1231         WHERE  reserve_id = ?
1232     ";
1233     my $sth_upd = $dbh->prepare($query);
1234     $sth_upd->execute( $itemnumber, $reserve_id );
1235     # second step update all others reserves
1236     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1237 }
1238
1239 =head2 IsAvailableForItemLevelRequest
1240
1241   my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1242
1243 Checks whether a given item record is available for an
1244 item-level hold request.  An item is available if
1245
1246 * it is not lost AND
1247 * it is not damaged AND
1248 * it is not withdrawn AND
1249 * a waiting or in transit reserve is placed on
1250 * does not have a not for loan value > 0
1251
1252 Need to check the issuingrules onshelfholds column,
1253 if this is set items on the shelf can be placed on hold
1254
1255 Note that IsAvailableForItemLevelRequest() does not
1256 check if the staff operator is authorized to place
1257 a request on the item - in particular,
1258 this routine does not check IndependentBranches
1259 and canreservefromotherbranches.
1260
1261 Note also that this subroutine does not checks smart
1262 rules limits for item by reservesallowed/holds_per_record
1263 values, this complemented in calling code with calls and
1264 checks with CanItemBeReserved or CanBookBeReserved.
1265
1266 =cut
1267
1268 sub IsAvailableForItemLevelRequest {
1269     my $item                = shift;
1270     my $patron              = shift;
1271     my $pickup_branchcode   = shift;
1272     # items_any_available is precalculated status passed from request.pl when set of items
1273     # looped outside of IsAvailableForItemLevelRequest to avoid nested loops:
1274     my $items_any_available = shift;
1275
1276     my $dbh = C4::Context->dbh;
1277     # must check the notforloan setting of the itemtype
1278     # FIXME - a lot of places in the code do this
1279     #         or something similar - need to be
1280     #         consolidated
1281     my $itemtype = $item->effective_itemtype;
1282     my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1283
1284     return 0 if
1285         $notforloan_per_itemtype ||
1286         $item->itemlost        ||
1287         $item->notforloan > 0  ||
1288         $item->withdrawn        ||
1289         ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1290
1291     if ($pickup_branchcode) {
1292         my $destination = Koha::Libraries->find($pickup_branchcode);
1293         return 0 unless $destination;
1294         return 0 unless $destination->pickup_location;
1295         return 0 unless $item->can_be_transferred( { to => $destination } );
1296         my $reserves_control_branch =
1297             GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
1298         my $branchitemrule =
1299             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
1300         my $home_library = Koka::Libraries->find( {branchcode => $item->homebranch} );
1301         return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
1302     }
1303
1304     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1305
1306     if ( $on_shelf_holds == 1 ) {
1307         return 1;
1308     } elsif ( $on_shelf_holds == 2 ) {
1309
1310         # if we have this param predefined from outer caller sub, we just need
1311         # to return it, so we saving from having loop inside other loop:
1312         return  $items_any_available ? 0 : 1
1313             if defined $items_any_available;
1314
1315         my $any_available = ItemsAnyAvailableAndNotRestricted( { biblionumber => $item->biblionumber, patron => $patron });
1316         return $any_available ? 0 : 1;
1317     } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1318         return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1319     }
1320 }
1321
1322 =head2 ItemsAnyAvailableAndNotRestricted
1323
1324   ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron });
1325
1326 This function checks all items for specified biblionumber (numeric) against patron (object)
1327 and returns true (1) if at least one item available for loan/check out/present/not held
1328 and also checks other parameters logic which not restricts item for hold at all (for ex.
1329 AllowHoldsOnDamagedItems or 'holdallowed' own/sibling library)
1330
1331 =cut
1332
1333 sub ItemsAnyAvailableAndNotRestricted {
1334     my $param = shift;
1335
1336     my @items = Koha::Items->search( { biblionumber => $param->{biblionumber} } );
1337
1338     foreach my $i (@items) {
1339         my $reserves_control_branch =
1340             GetReservesControlBranch( $i->unblessed(), $param->{patron}->unblessed );
1341         my $branchitemrule =
1342             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1343         my $item_library = Koha::Libraries->find( { branchcode => $i->homebranch } );
1344
1345         # we can return (end the loop) when first one found:
1346         return 1
1347             unless $i->itemlost
1348             || $i->notforloan > 0
1349             || $i->withdrawn
1350             || $i->onloan
1351             || IsItemOnHoldAndFound( $i->id )
1352             || ( $i->damaged
1353                  && ! C4::Context->preference('AllowHoldsOnDamagedItems') )
1354             || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1355             || $branchitemrule->{holdallowed} == 1 && $param->{patron}->branchcode ne $i->homebranch
1356             || $branchitemrule->{holdallowed} == 3 && ! $item_library->validate_hold_sibling( { branchcode => $param->{patron}->branchcode } )
1357             || CanItemBeReserved( $param->{patron}->borrowernumber, $i->id )->{status} ne 'OK';
1358     }
1359
1360     return 0;
1361 }
1362
1363 =head2 AlterPriority
1364
1365   AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1366
1367 This function changes a reserve's priority up, down, to the top, or to the bottom.
1368 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1369
1370 =cut
1371
1372 sub AlterPriority {
1373     my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1374
1375     my $hold = Koha::Holds->find( $reserve_id );
1376     return unless $hold;
1377
1378     if ( $hold->cancellationdate ) {
1379         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1380         return;
1381     }
1382
1383     if ( $where eq 'up' ) {
1384       return unless $prev_priority;
1385       _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1386     } elsif ( $where eq 'down' ) {
1387       return unless $next_priority;
1388       _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1389     } elsif ( $where eq 'top' ) {
1390       _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1391     } elsif ( $where eq 'bottom' ) {
1392       _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1393     }
1394
1395     # FIXME Should return the new priority
1396 }
1397
1398 =head2 ToggleLowestPriority
1399
1400   ToggleLowestPriority( $borrowernumber, $biblionumber );
1401
1402 This function sets the lowestPriority field to true if is false, and false if it is true.
1403
1404 =cut
1405
1406 sub ToggleLowestPriority {
1407     my ( $reserve_id ) = @_;
1408
1409     my $dbh = C4::Context->dbh;
1410
1411     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1412     $sth->execute( $reserve_id );
1413
1414     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1415 }
1416
1417 =head2 ToggleSuspend
1418
1419   ToggleSuspend( $reserve_id );
1420
1421 This function sets the suspend field to true if is false, and false if it is true.
1422 If the reserve is currently suspended with a suspend_until date, that date will
1423 be cleared when it is unsuspended.
1424
1425 =cut
1426
1427 sub ToggleSuspend {
1428     my ( $reserve_id, $suspend_until ) = @_;
1429
1430     $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1431
1432     my $hold = Koha::Holds->find( $reserve_id );
1433
1434     if ( $hold->is_suspended ) {
1435         $hold->resume()
1436     } else {
1437         $hold->suspend_hold( $suspend_until );
1438     }
1439 }
1440
1441 =head2 SuspendAll
1442
1443   SuspendAll(
1444       borrowernumber   => $borrowernumber,
1445       [ biblionumber   => $biblionumber, ]
1446       [ suspend_until  => $suspend_until, ]
1447       [ suspend        => $suspend ]
1448   );
1449
1450   This function accepts a set of hash keys as its parameters.
1451   It requires either borrowernumber or biblionumber, or both.
1452
1453   suspend_until is wholly optional.
1454
1455 =cut
1456
1457 sub SuspendAll {
1458     my %params = @_;
1459
1460     my $borrowernumber = $params{'borrowernumber'} || undef;
1461     my $biblionumber   = $params{'biblionumber'}   || undef;
1462     my $suspend_until  = $params{'suspend_until'}  || undef;
1463     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1464
1465     $suspend_until = eval { dt_from_string($suspend_until) }
1466       if ( defined($suspend_until) );
1467
1468     return unless ( $borrowernumber || $biblionumber );
1469
1470     my $params;
1471     $params->{found}          = undef;
1472     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1473     $params->{biblionumber}   = $biblionumber if $biblionumber;
1474
1475     my @holds = Koha::Holds->search($params);
1476
1477     if ($suspend) {
1478         map { $_->suspend_hold($suspend_until) } @holds;
1479     }
1480     else {
1481         map { $_->resume() } @holds;
1482     }
1483 }
1484
1485
1486 =head2 _FixPriority
1487
1488   _FixPriority({
1489     reserve_id => $reserve_id,
1490     [rank => $rank,]
1491     [ignoreSetLowestRank => $ignoreSetLowestRank]
1492   });
1493
1494   or
1495
1496   _FixPriority({ biblionumber => $biblionumber});
1497
1498 This routine adjusts the priority of a hold request and holds
1499 on the same bib.
1500
1501 In the first form, where a reserve_id is passed, the priority of the
1502 hold is set to supplied rank, and other holds for that bib are adjusted
1503 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1504 is supplied, all of the holds on that bib have their priority adjusted
1505 as if the second form had been used.
1506
1507 In the second form, where a biblionumber is passed, the holds on that
1508 bib (that are not captured) are sorted in order of increasing priority,
1509 then have reserves.priority set so that the first non-captured hold
1510 has its priority set to 1, the second non-captured hold has its priority
1511 set to 2, and so forth.
1512
1513 In both cases, holds that have the lowestPriority flag on are have their
1514 priority adjusted to ensure that they remain at the end of the line.
1515
1516 Note that the ignoreSetLowestRank parameter is meant to be used only
1517 when _FixPriority calls itself.
1518
1519 =cut
1520
1521 sub _FixPriority {
1522     my ( $params ) = @_;
1523     my $reserve_id = $params->{reserve_id};
1524     my $rank = $params->{rank} // '';
1525     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1526     my $biblionumber = $params->{biblionumber};
1527
1528     my $dbh = C4::Context->dbh;
1529
1530     my $hold;
1531     if ( $reserve_id ) {
1532         $hold = Koha::Holds->find( $reserve_id );
1533         if (!defined $hold){
1534             # may have already been checked out and hold fulfilled
1535             $hold = Koha::Old::Holds->find( $reserve_id );
1536         }
1537         return unless $hold;
1538     }
1539
1540     unless ( $biblionumber ) { # FIXME This is a very weird API
1541         $biblionumber = $hold->biblionumber;
1542     }
1543
1544     if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1545         $hold->cancel;
1546     }
1547     elsif ( $reserve_id && ( $rank eq "W" || $rank eq "0" ) ) {
1548
1549         # make sure priority for waiting or in-transit items is 0
1550         my $query = "
1551             UPDATE reserves
1552             SET    priority = 0
1553             WHERE reserve_id = ?
1554             AND found IN ('W', 'T')
1555         ";
1556         my $sth = $dbh->prepare($query);
1557         $sth->execute( $reserve_id );
1558     }
1559     my @priority;
1560
1561     # get whats left
1562     my $query = "
1563         SELECT reserve_id, borrowernumber, reservedate
1564         FROM   reserves
1565         WHERE  biblionumber   = ?
1566           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1567         ORDER BY priority ASC
1568     ";
1569     my $sth = $dbh->prepare($query);
1570     $sth->execute( $biblionumber );
1571     while ( my $line = $sth->fetchrow_hashref ) {
1572         push( @priority,     $line );
1573     }
1574
1575     # FIXME This whole sub must be rewritten, especially to highlight what is done when reserve_id is not given
1576     # To find the matching index
1577     my $i;
1578     my $key = -1;    # to allow for 0 to be a valid result
1579     for ( $i = 0 ; $i < @priority ; $i++ ) {
1580         if ( $reserve_id && $reserve_id == $priority[$i]->{'reserve_id'} ) {
1581             $key = $i;    # save the index
1582             last;
1583         }
1584     }
1585
1586     # if index exists in array then move it to new position
1587     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1588         my $new_rank = $rank -
1589           1;    # $new_rank is what you want the new index to be in the array
1590         my $moving_item = splice( @priority, $key, 1 );
1591         splice( @priority, $new_rank, 0, $moving_item );
1592     }
1593
1594     # now fix the priority on those that are left....
1595     $query = "
1596         UPDATE reserves
1597         SET    priority = ?
1598         WHERE  reserve_id = ?
1599     ";
1600     $sth = $dbh->prepare($query);
1601     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1602         $sth->execute(
1603             $j + 1,
1604             $priority[$j]->{'reserve_id'}
1605         );
1606     }
1607
1608     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1609     $sth->execute();
1610
1611     unless ( $ignoreSetLowestRank ) {
1612       while ( my $res = $sth->fetchrow_hashref() ) {
1613         _FixPriority({
1614             reserve_id => $res->{'reserve_id'},
1615             rank => '999999',
1616             ignoreSetLowestRank => 1
1617         });
1618       }
1619     }
1620 }
1621
1622 =head2 _Findgroupreserve
1623
1624   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1625
1626 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1627 first match found.  If neither, then we look for non-holds-queue based holds.
1628 Lookahead is the number of days to look in advance.
1629
1630 C<&_Findgroupreserve> returns :
1631 C<@results> is an array of references-to-hash whose keys are mostly
1632 fields from the reserves table of the Koha database, plus
1633 C<biblioitemnumber>.
1634
1635 This routine with either return:
1636 1 - Item specific holds from the holds queue
1637 2 - Title level holds from the holds queue
1638 3 - All holds for this biblionumber
1639
1640 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1641
1642 =cut
1643
1644 sub _Findgroupreserve {
1645     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1646     my $dbh   = C4::Context->dbh;
1647
1648     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1649     # check for exact targeted match
1650     my $item_level_target_query = qq{
1651         SELECT reserves.biblionumber        AS biblionumber,
1652                reserves.borrowernumber      AS borrowernumber,
1653                reserves.reservedate         AS reservedate,
1654                reserves.branchcode          AS branchcode,
1655                reserves.cancellationdate    AS cancellationdate,
1656                reserves.found               AS found,
1657                reserves.reservenotes        AS reservenotes,
1658                reserves.priority            AS priority,
1659                reserves.timestamp           AS timestamp,
1660                biblioitems.biblioitemnumber AS biblioitemnumber,
1661                reserves.itemnumber          AS itemnumber,
1662                reserves.reserve_id          AS reserve_id,
1663                reserves.itemtype            AS itemtype,
1664                reserves.non_priority        AS non_priority
1665         FROM reserves
1666         JOIN biblioitems USING (biblionumber)
1667         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1668         WHERE found IS NULL
1669         AND priority > 0
1670         AND item_level_request = 1
1671         AND itemnumber = ?
1672         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1673         AND suspend = 0
1674         ORDER BY priority
1675     };
1676     my $sth = $dbh->prepare($item_level_target_query);
1677     $sth->execute($itemnumber, $lookahead||0);
1678     my @results;
1679     if ( my $data = $sth->fetchrow_hashref ) {
1680         push( @results, $data )
1681           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1682     }
1683     return @results if @results;
1684
1685     # check for title-level targeted match
1686     my $title_level_target_query = qq{
1687         SELECT reserves.biblionumber        AS biblionumber,
1688                reserves.borrowernumber      AS borrowernumber,
1689                reserves.reservedate         AS reservedate,
1690                reserves.branchcode          AS branchcode,
1691                reserves.cancellationdate    AS cancellationdate,
1692                reserves.found               AS found,
1693                reserves.reservenotes        AS reservenotes,
1694                reserves.priority            AS priority,
1695                reserves.timestamp           AS timestamp,
1696                biblioitems.biblioitemnumber AS biblioitemnumber,
1697                reserves.itemnumber          AS itemnumber,
1698                reserves.reserve_id          AS reserve_id,
1699                reserves.itemtype            AS itemtype,
1700                reserves.non_priority        AS non_priority
1701         FROM reserves
1702         JOIN biblioitems USING (biblionumber)
1703         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1704         WHERE found IS NULL
1705         AND priority > 0
1706         AND item_level_request = 0
1707         AND hold_fill_targets.itemnumber = ?
1708         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1709         AND suspend = 0
1710         ORDER BY priority
1711     };
1712     $sth = $dbh->prepare($title_level_target_query);
1713     $sth->execute($itemnumber, $lookahead||0);
1714     @results = ();
1715     if ( my $data = $sth->fetchrow_hashref ) {
1716         push( @results, $data )
1717           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1718     }
1719     return @results if @results;
1720
1721     my $query = qq{
1722         SELECT reserves.biblionumber               AS biblionumber,
1723                reserves.borrowernumber             AS borrowernumber,
1724                reserves.reservedate                AS reservedate,
1725                reserves.waitingdate                AS waitingdate,
1726                reserves.branchcode                 AS branchcode,
1727                reserves.cancellationdate           AS cancellationdate,
1728                reserves.found                      AS found,
1729                reserves.reservenotes               AS reservenotes,
1730                reserves.priority                   AS priority,
1731                reserves.timestamp                  AS timestamp,
1732                reserves.itemnumber                 AS itemnumber,
1733                reserves.reserve_id                 AS reserve_id,
1734                reserves.itemtype                   AS itemtype,
1735                reserves.non_priority        AS non_priority
1736         FROM reserves
1737         WHERE reserves.biblionumber = ?
1738           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1739           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1740           AND suspend = 0
1741           ORDER BY priority
1742     };
1743     $sth = $dbh->prepare($query);
1744     $sth->execute( $biblio, $itemnumber, $lookahead||0);
1745     @results = ();
1746     while ( my $data = $sth->fetchrow_hashref ) {
1747         push( @results, $data )
1748           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1749     }
1750     return @results;
1751 }
1752
1753 =head2 _koha_notify_reserve
1754
1755   _koha_notify_reserve( $hold->reserve_id );
1756
1757 Sends a notification to the patron that their hold has been filled (through
1758 ModReserveAffect, _not_ ModReserveFill)
1759
1760 The letter code for this notice may be found using the following query:
1761
1762     select distinct letter_code
1763     from message_transports
1764     inner join message_attributes using (message_attribute_id)
1765     where message_name = 'Hold_Filled'
1766
1767 This will probably sipmly be 'HOLD', but because it is defined in the database,
1768 it is subject to addition or change.
1769
1770 The following tables are availalbe witin the notice:
1771
1772     branches
1773     borrowers
1774     biblio
1775     biblioitems
1776     reserves
1777     items
1778
1779 =cut
1780
1781 sub _koha_notify_reserve {
1782     my $reserve_id = shift;
1783     my $hold = Koha::Holds->find($reserve_id);
1784     my $borrowernumber = $hold->borrowernumber;
1785
1786     my $patron = Koha::Patrons->find( $borrowernumber );
1787
1788     # Try to get the borrower's email address
1789     my $to_address = $patron->notice_email_address;
1790
1791     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1792             borrowernumber => $borrowernumber,
1793             message_name => 'Hold_Filled'
1794     } );
1795
1796     my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1797
1798     my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1799
1800     my %letter_params = (
1801         module => 'reserves',
1802         branchcode => $hold->branchcode,
1803         lang => $patron->lang,
1804         tables => {
1805             'branches'       => $library,
1806             'borrowers'      => $patron->unblessed,
1807             'biblio'         => $hold->biblionumber,
1808             'biblioitems'    => $hold->biblionumber,
1809             'reserves'       => $hold->unblessed,
1810             'items'          => $hold->itemnumber,
1811         },
1812     );
1813
1814     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.
1815     my $send_notification = sub {
1816         my ( $mtt, $letter_code ) = (@_);
1817         return unless defined $letter_code;
1818         $letter_params{letter_code} = $letter_code;
1819         $letter_params{message_transport_type} = $mtt;
1820         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1821         unless ($letter) {
1822             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1823             return;
1824         }
1825
1826         C4::Letters::EnqueueLetter( {
1827             letter => $letter,
1828             borrowernumber => $borrowernumber,
1829             from_address => $admin_email_address,
1830             message_transport_type => $mtt,
1831         } );
1832     };
1833
1834     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1835         next if (
1836                ( $mtt eq 'email' and not $to_address ) # No email address
1837             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
1838             or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1839         );
1840
1841         &$send_notification($mtt, $letter_code);
1842         $notification_sent++;
1843     }
1844     #Making sure that a print notification is sent if no other transport types can be utilized.
1845     if (! $notification_sent) {
1846         &$send_notification('print', 'HOLD');
1847     }
1848
1849 }
1850
1851 =head2 _ShiftPriorityByDateAndPriority
1852
1853   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1854
1855 This increments the priority of all reserves after the one
1856 with either the lowest date after C<$reservedate>
1857 or the lowest priority after C<$priority>.
1858
1859 It effectively makes room for a new reserve to be inserted with a certain
1860 priority, which is returned.
1861
1862 This is most useful when the reservedate can be set by the user.  It allows
1863 the new reserve to be placed before other reserves that have a later
1864 reservedate.  Since priority also is set by the form in reserves/request.pl
1865 the sub accounts for that too.
1866
1867 =cut
1868
1869 sub _ShiftPriorityByDateAndPriority {
1870     my ( $biblio, $resdate, $new_priority ) = @_;
1871
1872     my $dbh = C4::Context->dbh;
1873     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1874     my $sth = $dbh->prepare( $query );
1875     $sth->execute( $biblio, $resdate, $new_priority );
1876     my $min_priority = $sth->fetchrow;
1877     # if no such matches are found, $new_priority remains as original value
1878     $new_priority = $min_priority if ( $min_priority );
1879
1880     # Shift the priority up by one; works in conjunction with the next SQL statement
1881     $query = "UPDATE reserves
1882               SET priority = priority+1
1883               WHERE biblionumber = ?
1884               AND borrowernumber = ?
1885               AND reservedate = ?
1886               AND found IS NULL";
1887     my $sth_update = $dbh->prepare( $query );
1888
1889     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1890     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1891     $sth = $dbh->prepare( $query );
1892     $sth->execute( $new_priority, $biblio );
1893     while ( my $row = $sth->fetchrow_hashref ) {
1894         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1895     }
1896
1897     return $new_priority;  # so the caller knows what priority they wind up receiving
1898 }
1899
1900 =head2 MoveReserve
1901
1902   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1903
1904 Use when checking out an item to handle reserves
1905 If $cancelreserve boolean is set to true, it will remove existing reserve
1906
1907 =cut
1908
1909 sub MoveReserve {
1910     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1911
1912     $cancelreserve //= 0;
1913
1914     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1915     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1916     return unless $res;
1917
1918     my $biblionumber     =  $res->{biblionumber};
1919
1920     if ($res->{borrowernumber} == $borrowernumber) {
1921         ModReserveFill($res);
1922     }
1923     else {
1924         # warn "Reserved";
1925         # The item is reserved by someone else.
1926         # Find this item in the reserves
1927
1928         my $borr_res  = Koha::Holds->search({
1929             borrowernumber => $borrowernumber,
1930             biblionumber   => $biblionumber,
1931         },{
1932             order_by       => 'priority'
1933         })->next();
1934
1935         if ( $borr_res ) {
1936             # The item is reserved by the current patron
1937             ModReserveFill($borr_res->unblessed);
1938         }
1939
1940         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1941             RevertWaitingStatus({ itemnumber => $itemnumber });
1942         }
1943         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1944             my $hold = Koha::Holds->find( $res->{reserve_id} );
1945             $hold->cancel;
1946         }
1947     }
1948 }
1949
1950 =head2 MergeHolds
1951
1952   MergeHolds($dbh,$to_biblio, $from_biblio);
1953
1954 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1955
1956 =cut
1957
1958 sub MergeHolds {
1959     my ( $dbh, $to_biblio, $from_biblio ) = @_;
1960     my $sth = $dbh->prepare(
1961         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1962     );
1963     $sth->execute($from_biblio);
1964     if ( my $data = $sth->fetchrow_hashref() ) {
1965
1966         # holds exist on old record, if not we don't need to do anything
1967         $sth = $dbh->prepare(
1968             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1969         $sth->execute( $to_biblio, $from_biblio );
1970
1971         # Reorder by date
1972         # don't reorder those already waiting
1973
1974         $sth = $dbh->prepare(
1975 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1976         );
1977         my $upd_sth = $dbh->prepare(
1978 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1979         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1980         );
1981         $sth->execute( $to_biblio, 'W', 'T' );
1982         my $priority = 1;
1983         while ( my $reserve = $sth->fetchrow_hashref() ) {
1984             $upd_sth->execute(
1985                 $priority,                    $to_biblio,
1986                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1987                 $reserve->{'itemnumber'}
1988             );
1989             $priority++;
1990         }
1991     }
1992 }
1993
1994 =head2 RevertWaitingStatus
1995
1996   RevertWaitingStatus({ itemnumber => $itemnumber });
1997
1998   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1999
2000   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2001           item level hold, even if it was only a bibliolevel hold to
2002           begin with. This is because we can no longer know if a hold
2003           was item-level or bib-level after a hold has been set to
2004           waiting status.
2005
2006 =cut
2007
2008 sub RevertWaitingStatus {
2009     my ( $params ) = @_;
2010     my $itemnumber = $params->{'itemnumber'};
2011
2012     return unless ( $itemnumber );
2013
2014     my $dbh = C4::Context->dbh;
2015
2016     ## Get the waiting reserve we want to revert
2017     my $hold = Koha::Holds->search(
2018         {
2019             itemnumber => $itemnumber,
2020             found => { not => undef },
2021         }
2022     )->next;
2023
2024     ## Increment the priority of all other non-waiting
2025     ## reserves for this bib record
2026     my $holds = Koha::Holds->search({ biblionumber => $hold->biblionumber, priority => { '>' => 0 } })
2027                            ->update({ priority => \'priority + 1' }, { no_triggers => 1 });
2028
2029     ## Fix up the currently waiting reserve
2030     $hold->set(
2031         {
2032             priority    => 1,
2033             found       => undef,
2034             waitingdate => undef,
2035             itemnumber  => $hold->item_level_hold ? $hold->itemnumber : undef,
2036         }
2037     )->store();
2038
2039     _FixPriority( { biblionumber => $hold->biblionumber } );
2040
2041     return $hold;
2042 }
2043
2044 =head2 ReserveSlip
2045
2046 ReserveSlip(
2047     {
2048         branchcode     => $branchcode,
2049         borrowernumber => $borrowernumber,
2050         biblionumber   => $biblionumber,
2051         [ itemnumber   => $itemnumber, ]
2052         [ barcode      => $barcode, ]
2053     }
2054   )
2055
2056 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2057
2058 The letter code will be HOLD_SLIP, and the following tables are
2059 available within the slip:
2060
2061     reserves
2062     branches
2063     borrowers
2064     biblio
2065     biblioitems
2066     items
2067
2068 =cut
2069
2070 sub ReserveSlip {
2071     my ($args) = @_;
2072     my $branchcode     = $args->{branchcode};
2073     my $reserve_id = $args->{reserve_id};
2074
2075     my $hold = Koha::Holds->find($reserve_id);
2076     return unless $hold;
2077
2078     my $patron = $hold->borrower;
2079     my $reserve = $hold->unblessed;
2080
2081     return  C4::Letters::GetPreparedLetter (
2082         module => 'circulation',
2083         letter_code => 'HOLD_SLIP',
2084         branchcode => $branchcode,
2085         lang => $patron->lang,
2086         tables => {
2087             'reserves'    => $reserve,
2088             'branches'    => $reserve->{branchcode},
2089             'borrowers'   => $reserve->{borrowernumber},
2090             'biblio'      => $reserve->{biblionumber},
2091             'biblioitems' => $reserve->{biblionumber},
2092             'items'       => $reserve->{itemnumber},
2093         },
2094     );
2095 }
2096
2097 =head2 GetReservesControlBranch
2098
2099   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2100
2101   Return the branchcode to be used to determine which reserves
2102   policy applies to a transaction.
2103
2104   C<$item> is a hashref for an item. Only 'homebranch' is used.
2105
2106   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2107
2108 =cut
2109
2110 sub GetReservesControlBranch {
2111     my ( $item, $borrower ) = @_;
2112
2113     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2114
2115     my $branchcode =
2116         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2117       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2118       :                                              undef;
2119
2120     return $branchcode;
2121 }
2122
2123 =head2 CalculatePriority
2124
2125     my $p = CalculatePriority($biblionumber, $resdate);
2126
2127 Calculate priority for a new reserve on biblionumber, placing it at
2128 the end of the line of all holds whose start date falls before
2129 the current system time and that are neither on the hold shelf
2130 or in transit.
2131
2132 The reserve date parameter is optional; if it is supplied, the
2133 priority is based on the set of holds whose start date falls before
2134 the parameter value.
2135
2136 After calculation of this priority, it is recommended to call
2137 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2138 AddReserves.
2139
2140 =cut
2141
2142 sub CalculatePriority {
2143     my ( $biblionumber, $resdate ) = @_;
2144
2145     my $sql = q{
2146         SELECT COUNT(*) FROM reserves
2147         WHERE biblionumber = ?
2148         AND   priority > 0
2149         AND   (found IS NULL OR found = '')
2150     };
2151     #skip found==W or found==T (waiting or transit holds)
2152     if( $resdate ) {
2153         $sql.= ' AND ( reservedate <= ? )';
2154     }
2155     else {
2156         $sql.= ' AND ( reservedate < NOW() )';
2157     }
2158     my $dbh = C4::Context->dbh();
2159     my @row = $dbh->selectrow_array(
2160         $sql,
2161         undef,
2162         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2163     );
2164
2165     return @row ? $row[0]+1 : 1;
2166 }
2167
2168 =head2 IsItemOnHoldAndFound
2169
2170     my $bool = IsItemFoundHold( $itemnumber );
2171
2172     Returns true if the item is currently on hold
2173     and that hold has a non-null found status ( W, T, etc. )
2174
2175 =cut
2176
2177 sub IsItemOnHoldAndFound {
2178     my ($itemnumber) = @_;
2179
2180     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2181
2182     my $found = $rs->count(
2183         {
2184             itemnumber => $itemnumber,
2185             found      => { '!=' => undef }
2186         }
2187     );
2188
2189     return $found;
2190 }
2191
2192 =head2 GetMaxPatronHoldsForRecord
2193
2194 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2195
2196 For multiple holds on a given record for a given patron, the max
2197 number of record level holds that a patron can be placed is the highest
2198 value of the holds_per_record rule for each item if the record for that
2199 patron. This subroutine finds and returns the highest holds_per_record
2200 rule value for a given patron id and record id.
2201
2202 =cut
2203
2204 sub GetMaxPatronHoldsForRecord {
2205     my ( $borrowernumber, $biblionumber ) = @_;
2206
2207     my $patron = Koha::Patrons->find($borrowernumber);
2208     my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2209
2210     my $controlbranch = C4::Context->preference('ReservesControlBranch');
2211
2212     my $categorycode = $patron->categorycode;
2213     my $branchcode;
2214     $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2215
2216     my $max = 0;
2217     foreach my $item (@items) {
2218         my $itemtype = $item->effective_itemtype();
2219
2220         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2221
2222         my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2223         my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2224         $max = $holds_per_record if $holds_per_record > $max;
2225     }
2226
2227     return $max;
2228 }
2229
2230 =head2 GetHoldRule
2231
2232 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2233
2234 Returns the matching hold related issuingrule fields for a given
2235 patron category, itemtype, and library.
2236
2237 =cut
2238
2239 sub GetHoldRule {
2240     my ( $categorycode, $itemtype, $branchcode ) = @_;
2241
2242     my $reservesallowed = Koha::CirculationRules->get_effective_rule(
2243         {
2244             itemtype     => $itemtype,
2245             categorycode => $categorycode,
2246             branchcode   => $branchcode,
2247             rule_name    => 'reservesallowed',
2248             order_by     => {
2249                 -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
2250             }
2251         }
2252     );
2253
2254     my $rules;
2255     if ( $reservesallowed ) {
2256         $rules->{reservesallowed} = $reservesallowed->rule_value;
2257         $rules->{itemtype}        = $reservesallowed->itemtype;
2258         $rules->{categorycode}    = $reservesallowed->categorycode;
2259         $rules->{branchcode}      = $reservesallowed->branchcode;
2260     }
2261
2262     my $holds_per_x_rules = Koha::CirculationRules->get_effective_rules(
2263         {
2264             itemtype     => $itemtype,
2265             categorycode => $categorycode,
2266             branchcode   => $branchcode,
2267             rules        => ['holds_per_record', 'holds_per_day'],
2268             order_by     => {
2269                 -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
2270             }
2271         }
2272     );
2273     $rules->{holds_per_record} = $holds_per_x_rules->{holds_per_record};
2274     $rules->{holds_per_day} = $holds_per_x_rules->{holds_per_day};
2275
2276     return $rules;
2277 }
2278
2279 =head1 AUTHOR
2280
2281 Koha Development Team <http://koha-community.org/>
2282
2283 =cut
2284
2285 1;