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