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