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