use Memoize because Memcache::Memoize is slow for me
[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 use List::MoreUtils qw( firstidx );
40
41 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
42
43 =head1 NAME
44
45 C4::Reserves - Koha functions for dealing with reservation.
46
47 =head1 SYNOPSIS
48
49   use C4::Reserves;
50
51 =head1 DESCRIPTION
52
53 This modules provides somes functions to deal with reservations.
54
55   Reserves are stored in reserves table.
56   The following columns contains important values :
57   - priority >0      : then the reserve is at 1st stage, and not yet affected to any item.
58              =0      : then the reserve is being dealed
59   - found : NULL       : means the patron requested the 1st available, and we haven't choosen the item
60             T(ransit)  : the reserve is linked to an item but is in transit to the pickup branch
61             W(aiting)  : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
62             F(inished) : the reserve has been completed, and is done
63   - itemnumber : empty : the reserve is still unaffected to an item
64                  filled: the reserve is attached to an item
65   The complete workflow is :
66   ==== 1st use case ====
67   patron request a document, 1st available :                      P >0, F=NULL, I=NULL
68   a library having it run "transfertodo", and clic on the list    
69          if there is no transfer to do, the reserve waiting
70          patron can pick it up                                    P =0, F=W,    I=filled 
71          if there is a transfer to do, write in branchtransfer    P =0, F=T,    I=filled
72            The pickup library recieve the book, it check in       P =0, F=W,    I=filled
73   The patron borrow the book                                      P =0, F=F,    I=filled
74   
75   ==== 2nd use case ====
76   patron requests a document, a given item,
77     If pickup is holding branch                                   P =0, F=W,   I=filled
78     If transfer needed, write in branchtransfer                   P =0, F=T,    I=filled
79         The pickup library receive the book, it checks it in      P =0, F=W,    I=filled
80   The patron borrow the book                                      P =0, F=F,    I=filled
81
82 =head1 FUNCTIONS
83
84 =cut
85
86 BEGIN {
87     # set the version for version checking
88     $VERSION = 3.01;
89     require Exporter;
90     @ISA = qw(Exporter);
91     @EXPORT = qw(
92         &AddReserve
93   
94         &GetReservesFromItemnumber
95         &GetReservesFromBiblionumber
96         &GetReservesFromBorrowernumber
97         &GetReservesForBranch
98         &GetReservesToBranch
99         &GetReserveCount
100         &GetReserveFee
101                 &GetReserveInfo
102         &GetReserveStatus
103         
104         &GetOtherReserves
105         
106         &ModReserveFill
107         &ModReserveAffect
108         &ModReserve
109         &ModReserveStatus
110         &ModReserveCancelAll
111         &ModReserveMinusPriority
112         
113         &CheckReserves
114         &CanBookBeReserved
115         &CanItemBeReserved
116         &CancelReserve
117         &CancelExpiredReserves
118
119         &IsAvailableForItemLevelRequest
120         
121         &AlterPriority
122         &ToggleLowestPriority
123     );
124     @EXPORT_OK = qw( MergeHolds );
125 }    
126
127 =head2 AddReserve
128
129     AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
130
131 =cut
132
133 sub AddReserve {
134     my (
135         $branch,    $borrowernumber, $biblionumber,
136         $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
137         $title,      $checkitem, $found
138     ) = @_;
139     my $fee =
140           GetReserveFee($borrowernumber, $biblionumber, $constraint,
141             $bibitems );
142     my $dbh     = C4::Context->dbh;
143     my $const   = lc substr( $constraint, 0, 1 );
144     $resdate = format_date_in_iso( $resdate ) if ( $resdate );
145     $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
146     if ($expdate) {
147         $expdate = format_date_in_iso( $expdate );
148     } else {
149         undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
150     }
151     if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
152         # Make room in reserves for this before those of a later reserve date
153         $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
154     }
155     my $waitingdate;
156
157     # If the reserv had the waiting status, we had the value of the resdate
158     if ( $found eq 'W' ) {
159         $waitingdate = $resdate;
160     }
161
162     #eval {
163     # updates take place here
164     if ( $fee > 0 ) {
165         my $nextacctno = &getnextacctno( $borrowernumber );
166         my $query      = qq/
167         INSERT INTO accountlines
168             (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
169         VALUES
170             (?,?,now(),?,?,'Res',?)
171     /;
172         my $usth = $dbh->prepare($query);
173         $usth->execute( $borrowernumber, $nextacctno, $fee,
174             "Reserve Charge - $title", $fee );
175     }
176
177     #if ($const eq 'a'){
178     my $query = qq/
179         INSERT INTO reserves
180             (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
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,
189         $const,          $priority,     $notes,   $checkitem,
190         $found,          $waitingdate,  $expdate
191     );
192
193     # Send e-mail to librarian if syspref is active
194     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
195         my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
196         my $biblio   = GetBiblioData($biblionumber);
197         my $letter = C4::Letters::getletter( 'reserves', 'HOLDPLACED');
198         my $branchcode = $borrower->{branchcode};
199         my $branch_details = C4::Branch::GetBranchDetail($branchcode);
200         my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
201
202         my %keys = (%$borrower, %$biblio);
203         foreach my $key (keys %keys) {
204             my $replacefield = "<<$key>>";
205             $letter->{content} =~ s/$replacefield/$keys{$key}/g;
206             $letter->{title} =~ s/$replacefield/$keys{$key}/g;
207         }
208         
209         C4::Letters::EnqueueLetter(
210                             {   letter                 => $letter,
211                                 borrowernumber         => $borrowernumber,
212                                 message_transport_type => 'email',
213                                 from_address           => $admin_email_address,
214                                 to_address           => $admin_email_address,
215                             }
216                         );
217         
218
219     }
220
221
222     #}
223     ($const eq "o" || $const eq "e") or return;   # FIXME: why not have a useful return value?
224     $query = qq/
225         INSERT INTO reserveconstraints
226             (borrowernumber,biblionumber,reservedate,biblioitemnumber)
227         VALUES
228             (?,?,?,?)
229     /;
230     $sth = $dbh->prepare($query);    # keep prepare outside the loop!
231     foreach (@$bibitems) {
232         $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
233     }
234         
235     return;     # FIXME: why not have a useful return value?
236 }
237
238 =head2 GetReservesFromBiblionumber
239
240   ($count, $title_reserves) = &GetReserves($biblionumber);
241
242 This function gets the list of reservations for one C<$biblionumber>, returning a count
243 of the reserves and an arrayref pointing to the reserves for C<$biblionumber>.
244
245 =cut
246
247 sub GetReservesFromBiblionumber {
248     my ($biblionumber) = shift or return (0, []);
249     my ($all_dates) = shift;
250     my $dbh   = C4::Context->dbh;
251
252     # Find the desired items in the reserves
253     my $query = "
254         SELECT  branchcode,
255                 timestamp AS rtimestamp,
256                 priority,
257                 biblionumber,
258                 borrowernumber,
259                 reservedate,
260                 constrainttype,
261                 found,
262                 itemnumber,
263                 reservenotes,
264                 expirationdate,
265                 lowestPriority
266         FROM     reserves
267         WHERE biblionumber = ? ";
268     unless ( $all_dates ) {
269         $query .= "AND reservedate <= CURRENT_DATE()";
270     }
271     $query .= "ORDER BY priority";
272     my $sth = $dbh->prepare($query);
273     $sth->execute($biblionumber);
274     my @results;
275     my $i = 0;
276     while ( my $data = $sth->fetchrow_hashref ) {
277
278         # FIXME - What is this doing? How do constraints work?
279         if ($data->{constrainttype} eq 'o') {
280             $query = '
281                 SELECT biblioitemnumber
282                 FROM  reserveconstraints
283                 WHERE  biblionumber   = ?
284                 AND   borrowernumber = ?
285                 AND   reservedate    = ?
286             ';
287             my $csth = $dbh->prepare($query);
288             $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
289             my @bibitemno;
290             while ( my $bibitemnos = $csth->fetchrow_array ) {
291                 push( @bibitemno, $bibitemnos );    # FIXME: inefficient: use fetchall_arrayref
292             }
293             my $count = scalar @bibitemno;
294     
295             # if we have two or more different specific itemtypes
296             # reserved by same person on same day
297             my $bdata;
298             if ( $count > 1 ) {
299                 $bdata = GetBiblioItemData( $bibitemno[$i] );   # FIXME: This doesn't make sense.
300                 $i++; #  $i can increase each pass, but the next @bibitemno might be smaller?
301             }
302             else {
303                 # Look up the book we just found.
304                 $bdata = GetBiblioItemData( $bibitemno[0] );
305             }
306             # Add the results of this latest search to the current
307             # results.
308             # FIXME - An 'each' would probably be more efficient.
309             foreach my $key ( keys %$bdata ) {
310                 $data->{$key} = $bdata->{$key};
311             }
312         }
313         push @results, $data;
314     }
315     return ( $#results + 1, \@results );
316 }
317
318 =head2 GetReservesFromItemnumber
319
320  ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber);
321
322 TODO :: Description here
323
324 =cut
325
326 sub GetReservesFromItemnumber {
327     my ( $itemnumber, $all_dates ) = @_;
328     my $dbh   = C4::Context->dbh;
329     my $query = "
330     SELECT reservedate,borrowernumber,branchcode
331     FROM   reserves
332     WHERE  itemnumber=?
333     ";
334     unless ( $all_dates ) {
335         $query .= " AND reservedate <= CURRENT_DATE()";
336     }
337     my $sth_res = $dbh->prepare($query);
338     $sth_res->execute($itemnumber);
339     my ( $reservedate, $borrowernumber,$branchcode ) = $sth_res->fetchrow_array;
340     return ( $reservedate, $borrowernumber, $branchcode );
341 }
342
343 =head2 GetReservesFromBorrowernumber
344
345     $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
346
347 TODO :: Descritpion
348
349 =cut
350
351 sub GetReservesFromBorrowernumber {
352     my ( $borrowernumber, $status ) = @_;
353     my $dbh   = C4::Context->dbh;
354     my $sth;
355     if ($status) {
356         $sth = $dbh->prepare("
357             SELECT *
358             FROM   reserves
359             WHERE  borrowernumber=?
360                 AND found =?
361             ORDER BY reservedate
362         ");
363         $sth->execute($borrowernumber,$status);
364     } else {
365         $sth = $dbh->prepare("
366             SELECT *
367             FROM   reserves
368             WHERE  borrowernumber=?
369             ORDER BY reservedate
370         ");
371         $sth->execute($borrowernumber);
372     }
373     my $data = $sth->fetchall_arrayref({});
374     return @$data;
375 }
376 #-------------------------------------------------------------------------------------
377 =head2 CanBookBeReserved
378
379   $error = &CanBookBeReserved($borrowernumber, $biblionumber)
380
381 =cut
382
383 sub CanBookBeReserved{
384     my ($borrowernumber, $biblionumber) = @_;
385
386     my @items = get_itemnumbers_of($biblionumber);
387     #get items linked via host records
388     my @hostitems = get_hostitemnumbers_of($biblionumber);
389     if (@hostitems){
390         push (@items,@hostitems);
391     }
392
393     foreach my $item (@items){
394         return 1 if CanItemBeReserved($borrowernumber, $item);
395     }
396     return 0;
397 }
398
399 =head2 CanItemBeReserved
400
401   $error = &CanItemBeReserved($borrowernumber, $itemnumber)
402
403 This function return 1 if an item can be issued by this borrower.
404
405 =cut
406
407 sub CanItemBeReserved{
408     my ($borrowernumber, $itemnumber) = @_;
409     
410     my $dbh             = C4::Context->dbh;
411     my $allowedreserves = 0;
412             
413     my $controlbranch = C4::Context->preference('ReservesControlBranch');
414     my $itype         = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype";
415
416     # we retrieve borrowers and items informations #
417     my $item     = GetItem($itemnumber);
418     my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);     
419     
420     # we retrieve user rights on this itemtype and branchcode
421     my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed 
422                              FROM issuingrules 
423                              WHERE (categorycode in (?,'*') ) 
424                              AND (itemtype IN (?,'*')) 
425                              AND (branchcode IN (?,'*')) 
426                              ORDER BY 
427                                categorycode DESC, 
428                                itemtype     DESC, 
429                                branchcode   DESC;"
430                            );
431                            
432     my $querycount ="SELECT 
433                             count(*) as count
434                             FROM reserves
435                                 LEFT JOIN items USING (itemnumber)
436                                 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
437                                 LEFT JOIN borrowers USING (borrowernumber)
438                             WHERE borrowernumber = ?
439                                 ";
440     
441     
442     my $itemtype     = $item->{$itype};
443     my $categorycode = $borrower->{categorycode};
444     my $branchcode   = "";
445     my $branchfield  = "reserves.branchcode";
446     
447     if( $controlbranch eq "ItemHomeLibrary" ){
448         $branchfield = "items.homebranch";
449         $branchcode = $item->{homebranch};
450     }elsif( $controlbranch eq "PatronLibrary" ){
451         $branchfield = "borrowers.branchcode";
452         $branchcode = $borrower->{branchcode};
453     }
454     
455     # we retrieve rights 
456     $sth->execute($categorycode, $itemtype, $branchcode);
457     if(my $rights = $sth->fetchrow_hashref()){
458         $itemtype        = $rights->{itemtype};
459         $allowedreserves = $rights->{reservesallowed}; 
460     }else{
461         $itemtype = '*';
462     }
463     
464     # we retrieve count
465     
466     $querycount .= "AND $branchfield = ?";
467     
468     $querycount .= " AND $itype = ?" if ($itemtype ne "*");
469     my $sthcount = $dbh->prepare($querycount);
470     
471     if($itemtype eq "*"){
472         $sthcount->execute($borrowernumber, $branchcode);
473     }else{
474         $sthcount->execute($borrowernumber, $branchcode, $itemtype);
475     }
476     
477     my $reservecount = "0";
478     if(my $rowcount = $sthcount->fetchrow_hashref()){
479         $reservecount = $rowcount->{count};
480     }
481     
482     # we check if it's ok or not
483     if( $reservecount < $allowedreserves ){
484         return 1;
485     }else{
486         return 0;
487     }
488 }
489 #--------------------------------------------------------------------------------
490 =head2 GetReserveCount
491
492   $number = &GetReserveCount($borrowernumber);
493
494 this function returns the number of reservation for a borrower given on input arg.
495
496 =cut
497
498 sub GetReserveCount {
499     my ($borrowernumber) = @_;
500
501     my $dbh = C4::Context->dbh;
502
503     my $query = '
504         SELECT COUNT(*) AS counter
505         FROM reserves
506           WHERE borrowernumber = ?
507     ';
508     my $sth = $dbh->prepare($query);
509     $sth->execute($borrowernumber);
510     my $row = $sth->fetchrow_hashref;
511     return $row->{counter};
512 }
513
514 =head2 GetOtherReserves
515
516   ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
517
518 Check queued list of this document and check if this document must be  transfered
519
520 =cut
521
522 sub GetOtherReserves {
523     my ($itemnumber) = @_;
524     my $messages;
525     my $nextreservinfo;
526     my ( $restype, $checkreserves ) = CheckReserves($itemnumber);
527     if ($checkreserves) {
528         my $iteminfo = GetItem($itemnumber);
529         if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
530             $messages->{'transfert'} = $checkreserves->{'branchcode'};
531             #minus priorities of others reservs
532             ModReserveMinusPriority(
533                 $itemnumber,
534                 $checkreserves->{'borrowernumber'},
535                 $iteminfo->{'biblionumber'}
536             );
537
538             #launch the subroutine dotransfer
539             C4::Items::ModItemTransfer(
540                 $itemnumber,
541                 $iteminfo->{'holdingbranch'},
542                 $checkreserves->{'branchcode'}
543               ),
544               ;
545         }
546
547      #step 2b : case of a reservation on the same branch, set the waiting status
548         else {
549             $messages->{'waiting'} = 1;
550             ModReserveMinusPriority(
551                 $itemnumber,
552                 $checkreserves->{'borrowernumber'},
553                 $iteminfo->{'biblionumber'}
554             );
555             ModReserveStatus($itemnumber,'W');
556         }
557
558         $nextreservinfo = $checkreserves->{'borrowernumber'};
559     }
560
561     return ( $messages, $nextreservinfo );
562 }
563
564 =head2 GetReserveFee
565
566   $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
567
568 Calculate the fee for a reserve
569
570 =cut
571
572 sub GetReserveFee {
573     my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
574
575     #check for issues;
576     my $dbh   = C4::Context->dbh;
577     my $const = lc substr( $constraint, 0, 1 );
578     my $query = qq/
579       SELECT * FROM borrowers
580     LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
581     WHERE borrowernumber = ?
582     /;
583     my $sth = $dbh->prepare($query);
584     $sth->execute($borrowernumber);
585     my $data = $sth->fetchrow_hashref;
586     $sth->finish();
587     my $fee      = $data->{'reservefee'};
588     my $cntitems = @- > $bibitems;
589
590     if ( $fee > 0 ) {
591
592         # check for items on issue
593         # first find biblioitem records
594         my @biblioitems;
595         my $sth1 = $dbh->prepare(
596             "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
597                    WHERE (biblio.biblionumber = ?)"
598         );
599         $sth1->execute($biblionumber);
600         while ( my $data1 = $sth1->fetchrow_hashref ) {
601             if ( $const eq "a" ) {
602                 push @biblioitems, $data1;
603             }
604             else {
605                 my $found = 0;
606                 my $x     = 0;
607                 while ( $x < $cntitems ) {
608                     if ( @$bibitems->{'biblioitemnumber'} ==
609                         $data->{'biblioitemnumber'} )
610                     {
611                         $found = 1;
612                     }
613                     $x++;
614                 }
615                 if ( $const eq 'o' ) {
616                     if ( $found == 1 ) {
617                         push @biblioitems, $data1;
618                     }
619                 }
620                 else {
621                     if ( $found == 0 ) {
622                         push @biblioitems, $data1;
623                     }
624                 }
625             }
626         }
627         $sth1->finish;
628         my $cntitemsfound = @biblioitems;
629         my $issues        = 0;
630         my $x             = 0;
631         my $allissued     = 1;
632         while ( $x < $cntitemsfound ) {
633             my $bitdata = $biblioitems[$x];
634             my $sth2    = $dbh->prepare(
635                 "SELECT * FROM items
636                      WHERE biblioitemnumber = ?"
637             );
638             $sth2->execute( $bitdata->{'biblioitemnumber'} );
639             while ( my $itdata = $sth2->fetchrow_hashref ) {
640                 my $sth3 = $dbh->prepare(
641                     "SELECT * FROM issues
642                        WHERE itemnumber = ?"
643                 );
644                 $sth3->execute( $itdata->{'itemnumber'} );
645                 if ( my $isdata = $sth3->fetchrow_hashref ) {
646                 }
647                 else {
648                     $allissued = 0;
649                 }
650             }
651             $x++;
652         }
653         if ( $allissued == 0 ) {
654             my $rsth =
655               $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
656             $rsth->execute($biblionumber);
657             if ( my $rdata = $rsth->fetchrow_hashref ) {
658             }
659             else {
660                 $fee = 0;
661             }
662         }
663     }
664     return $fee;
665 }
666
667 =head2 GetReservesToBranch
668
669   @transreserv = GetReservesToBranch( $frombranch );
670
671 Get reserve list for a given branch
672
673 =cut
674
675 sub GetReservesToBranch {
676     my ( $frombranch ) = @_;
677     my $dbh = C4::Context->dbh;
678     my $sth = $dbh->prepare(
679         "SELECT borrowernumber,reservedate,itemnumber,timestamp
680          FROM reserves 
681          WHERE priority='0' 
682            AND branchcode=?"
683     );
684     $sth->execute( $frombranch );
685     my @transreserv;
686     my $i = 0;
687     while ( my $data = $sth->fetchrow_hashref ) {
688         $transreserv[$i] = $data;
689         $i++;
690     }
691     return (@transreserv);
692 }
693
694 =head2 GetReservesForBranch
695
696   @transreserv = GetReservesForBranch($frombranch);
697
698 =cut
699
700 sub GetReservesForBranch {
701     my ($frombranch) = @_;
702     my $dbh          = C4::Context->dbh;
703         my $query        = "SELECT borrowernumber,reservedate,itemnumber,waitingdate
704         FROM   reserves 
705         WHERE   priority='0'
706             AND found='W' ";
707     if ($frombranch){
708         $query .= " AND branchcode=? ";
709         }
710     $query .= "ORDER BY waitingdate" ;
711     my $sth = $dbh->prepare($query);
712     if ($frombranch){
713                 $sth->execute($frombranch);
714         }
715     else {
716                 $sth->execute();
717         }
718     my @transreserv;
719     my $i = 0;
720     while ( my $data = $sth->fetchrow_hashref ) {
721         $transreserv[$i] = $data;
722         $i++;
723     }
724     return (@transreserv);
725 }
726
727 sub GetReserveStatus {
728     my ($itemnumber) = @_;
729     
730     my $dbh = C4::Context->dbh;
731     
732     my $itemstatus = $dbh->prepare("SELECT found FROM reserves WHERE itemnumber = ?");
733     
734     $itemstatus->execute($itemnumber);
735     my ($found) = $itemstatus->fetchrow_array;
736     return $found;
737 }
738
739 =head2 CheckReserves
740
741   ($status, $reserve) = &CheckReserves($itemnumber);
742   ($status, $reserve) = &CheckReserves(undef, $barcode);
743
744 Find a book in the reserves.
745
746 C<$itemnumber> is the book's item number.
747
748 As I understand it, C<&CheckReserves> looks for the given item in the
749 reserves. If it is found, that's a match, and C<$status> is set to
750 C<Waiting>.
751
752 Otherwise, it finds the most important item in the reserves with the
753 same biblio number as this book (I'm not clear on this) and returns it
754 with C<$status> set to C<Reserved>.
755
756 C<&CheckReserves> returns a two-element list:
757
758 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
759
760 C<$reserve> is the reserve item that matched. It is a
761 reference-to-hash whose keys are mostly the fields of the reserves
762 table in the Koha database.
763
764 =cut
765
766 our $_check_reserves;
767
768 sub CheckReserves {
769     my ( $item, $barcode ) = @_;
770
771         if ( defined $_check_reserves->{"$item $barcode"} ) {
772 warn "XXX _check_reserves $item $barcode\n";
773                 return @{ $_check_reserves->{"$item $barcode"} };
774         }
775         $_check_reserves->{"$item $barcode"} = [ 0,0 ]; # default
776
777     my $dbh = C4::Context->dbh;
778     my $sth;
779     my $select;
780     if (C4::Context->preference('item-level_itypes')){
781         $select = "
782            SELECT items.biblionumber,
783            items.biblioitemnumber,
784            itemtypes.notforloan,
785            items.notforloan AS itemnotforloan,
786            items.itemnumber
787            FROM   items
788            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
789            LEFT JOIN itemtypes   ON items.itype   = itemtypes.itemtype
790         ";
791     }
792     else {
793         $select = "
794            SELECT items.biblionumber,
795            items.biblioitemnumber,
796            itemtypes.notforloan,
797            items.notforloan AS itemnotforloan,
798            items.itemnumber
799            FROM   items
800            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
801            LEFT JOIN itemtypes   ON biblioitems.itemtype   = itemtypes.itemtype
802         ";
803     }
804    
805     if ($item) {
806         $sth = $dbh->prepare("$select WHERE itemnumber = ?");
807         $sth->execute($item);
808     }
809     else {
810         $sth = $dbh->prepare("$select WHERE barcode = ?");
811         $sth->execute($barcode);
812     }
813     # note: we get the itemnumber because we might have started w/ just the barcode.  Now we know for sure we have it.
814     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
815
816     return ( 0, 0 ) unless $itemnumber; # bail if we got nothing.
817
818     # if item is not for loan it cannot be reserved either.....
819     #    execpt where items.notforloan < 0 :  This indicates the item is holdable. 
820     return ( 0, 0 ) if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
821
822     # Find this item in the reserves
823     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber );
824
825     # $priority and $highest are used to find the most important item
826     # in the list returned by &_Findgroupreserve. (The lower $priority,
827     # the more important the item.)
828     # $highest is the most important item we've seen so far.
829     my $highest;
830     if (scalar @reserves) {
831         my $priority = 10000000;
832         foreach my $res (@reserves) {
833             if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
834                 return ( "Waiting", $res ); # Found it
835             } else {
836                 # See if this item is more important than what we've got so far
837                 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
838                     my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'});
839                     my $iteminfo=C4::Items::GetItem($itemnumber);
840                     my $branch=C4::Circulation::_GetCircControlBranch($iteminfo,$borrowerinfo);
841                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
842                     next if ($branchitemrule->{'holdallowed'} == 0);
843                     next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
844                     $priority = $res->{'priority'};
845                     $highest  = $res;
846                 }
847             }
848         }
849     }
850
851     # If we get this far, then no exact match was found.
852     # We return the most important (i.e. next) reservation.
853     if ($highest) {
854         $highest->{'itemnumber'} = $item;
855         $_check_reserves->{"item $barcode"} = [ "Reserved", $highest ]; # XXX cache
856         return ( "Reserved", $highest );
857     }
858     else {
859         return ( 0, 0 );
860     }
861 }
862
863 =head2 CancelExpiredReserves
864
865   CancelExpiredReserves();
866
867 Cancels all reserves with an expiration date from before today.
868
869 =cut
870
871 sub CancelExpiredReserves {
872
873     my $dbh = C4::Context->dbh;
874     my $sth = $dbh->prepare( "
875         SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() ) 
876         AND expirationdate IS NOT NULL
877     " );
878     $sth->execute();
879
880     while ( my $res = $sth->fetchrow_hashref() ) {
881         CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
882     }
883   
884 }
885
886 =head2 CancelReserve
887
888   &CancelReserve($biblionumber, $itemnumber, $borrowernumber);
889
890 Cancels a reserve.
891
892 Use either C<$biblionumber> or C<$itemnumber> to specify the item to
893 cancel, but not both: if both are given, C<&CancelReserve> does
894 nothing.
895
896 C<$borrowernumber> is the borrower number of the patron on whose
897 behalf the book was reserved.
898
899 If C<$biblionumber> was given, C<&CancelReserve> also adjusts the
900 priorities of the other people who are waiting on the book.
901
902 =cut
903
904 sub CancelReserve {
905     my ( $biblio, $item, $borr ) = @_;
906     my $dbh = C4::Context->dbh;
907         if ( $item and $borr ) {
908         # removing a waiting reserve record....
909         # update the database...
910         my $query = "
911             UPDATE reserves
912             SET    cancellationdate = now(),
913                    found            = Null,
914                    priority         = 0
915             WHERE  itemnumber       = ?
916              AND   borrowernumber   = ?
917         ";
918         my $sth = $dbh->prepare($query);
919         $sth->execute( $item, $borr );
920         $sth->finish;
921         $query = "
922             INSERT INTO old_reserves
923             SELECT * FROM reserves
924             WHERE  itemnumber       = ?
925              AND   borrowernumber   = ?
926         ";
927         $sth = $dbh->prepare($query);
928         $sth->execute( $item, $borr );
929         $query = "
930             DELETE FROM reserves
931             WHERE  itemnumber       = ?
932              AND   borrowernumber   = ?
933         ";
934         $sth = $dbh->prepare($query);
935         $sth->execute( $item, $borr );
936     }
937     else {
938         # removing a reserve record....
939         # get the prioritiy on this record....
940         my $priority;
941         my $query = qq/
942             SELECT priority FROM reserves
943             WHERE biblionumber   = ?
944               AND borrowernumber = ?
945               AND cancellationdate IS NULL
946               AND itemnumber IS NULL
947         /;
948         my $sth = $dbh->prepare($query);
949         $sth->execute( $biblio, $borr );
950         ($priority) = $sth->fetchrow_array;
951         $sth->finish;
952         $query = qq/
953             UPDATE reserves
954             SET    cancellationdate = now(),
955                    found            = Null,
956                    priority         = 0
957             WHERE  biblionumber     = ?
958               AND  borrowernumber   = ?
959         /;
960
961         # update the database, removing the record...
962         $sth = $dbh->prepare($query);
963         $sth->execute( $biblio, $borr );
964         $sth->finish;
965
966         $query = qq/
967             INSERT INTO old_reserves
968             SELECT * FROM reserves
969             WHERE  biblionumber     = ?
970               AND  borrowernumber   = ?
971         /;
972         $sth = $dbh->prepare($query);
973         $sth->execute( $biblio, $borr );
974
975         $query = qq/
976             DELETE FROM reserves
977             WHERE  biblionumber     = ?
978               AND  borrowernumber   = ?
979         /;
980         $sth = $dbh->prepare($query);
981         $sth->execute( $biblio, $borr );
982
983         # now fix the priority on the others....
984         _FixPriority( $biblio, $borr );
985     }
986 }
987
988 =head2 ModReserve
989
990   ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber])
991
992 Change a hold request's priority or cancel it.
993
994 C<$rank> specifies the effect of the change.  If C<$rank>
995 is 'W' or 'n', nothing happens.  This corresponds to leaving a
996 request alone when changing its priority in the holds queue
997 for a bib.
998
999 If C<$rank> is 'del', the hold request is cancelled.
1000
1001 If C<$rank> is an integer greater than zero, the priority of
1002 the request is set to that value.  Since priority != 0 means
1003 that the item is not waiting on the hold shelf, setting the 
1004 priority to a non-zero value also sets the request's found
1005 status and waiting date to NULL. 
1006
1007 The optional C<$itemnumber> parameter is used only when
1008 C<$rank> is a non-zero integer; if supplied, the itemnumber 
1009 of the hold request is set accordingly; if omitted, the itemnumber
1010 is cleared.
1011
1012 B<FIXME:> Note that the forgoing can have the effect of causing
1013 item-level hold requests to turn into title-level requests.  This
1014 will be fixed once reserves has separate columns for requested
1015 itemnumber and supplying itemnumber.
1016
1017 =cut
1018
1019 sub ModReserve {
1020     #subroutine to update a reserve
1021     my ( $rank, $biblio, $borrower, $branch , $itemnumber) = @_;
1022      return if $rank eq "W";
1023      return if $rank eq "n";
1024     my $dbh = C4::Context->dbh;
1025     if ( $rank eq "del" ) {
1026         my $query = qq/
1027             UPDATE reserves
1028             SET    cancellationdate=now()
1029             WHERE  biblionumber   = ?
1030              AND   borrowernumber = ?
1031         /;
1032         my $sth = $dbh->prepare($query);
1033         $sth->execute( $biblio, $borrower );
1034         $sth->finish;
1035         $query = qq/
1036             INSERT INTO old_reserves
1037             SELECT *
1038             FROM   reserves 
1039             WHERE  biblionumber   = ?
1040              AND   borrowernumber = ?
1041         /;
1042         $sth = $dbh->prepare($query);
1043         $sth->execute( $biblio, $borrower );
1044         $query = qq/
1045             DELETE FROM reserves 
1046             WHERE  biblionumber   = ?
1047              AND   borrowernumber = ?
1048         /;
1049         $sth = $dbh->prepare($query);
1050         $sth->execute( $biblio, $borrower );
1051         
1052     }
1053     elsif ($rank =~ /^\d+/ and $rank > 0) {
1054         my $query = qq/
1055         UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1056             WHERE biblionumber   = ?
1057              AND borrowernumber = ?
1058         /;
1059         my $sth = $dbh->prepare($query);
1060         $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower);
1061         $sth->finish;
1062         _FixPriority( $biblio, $borrower, $rank);
1063     }
1064 }
1065
1066 =head2 ModReserveFill
1067
1068   &ModReserveFill($reserve);
1069
1070 Fill a reserve. If I understand this correctly, this means that the
1071 reserved book has been found and given to the patron who reserved it.
1072
1073 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1074 whose keys are fields from the reserves table in the Koha database.
1075
1076 =cut
1077
1078 sub ModReserveFill {
1079     my ($res) = @_;
1080     my $dbh = C4::Context->dbh;
1081     # fill in a reserve record....
1082     my $biblionumber = $res->{'biblionumber'};
1083     my $borrowernumber    = $res->{'borrowernumber'};
1084     my $resdate = $res->{'reservedate'};
1085
1086     # get the priority on this record....
1087     my $priority;
1088     my $query = "SELECT priority
1089                  FROM   reserves
1090                  WHERE  biblionumber   = ?
1091                   AND   borrowernumber = ?
1092                   AND   reservedate    = ?";
1093     my $sth = $dbh->prepare($query);
1094     $sth->execute( $biblionumber, $borrowernumber, $resdate );
1095     ($priority) = $sth->fetchrow_array;
1096     $sth->finish;
1097
1098     # update the database...
1099     $query = "UPDATE reserves
1100                   SET    found            = 'F',
1101                          priority         = 0
1102                  WHERE  biblionumber     = ?
1103                     AND reservedate      = ?
1104                     AND borrowernumber   = ?
1105                 ";
1106     $sth = $dbh->prepare($query);
1107     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1108     $sth->finish;
1109
1110     # move to old_reserves
1111     $query = "INSERT INTO old_reserves
1112                  SELECT * FROM reserves
1113                  WHERE  biblionumber     = ?
1114                     AND reservedate      = ?
1115                     AND borrowernumber   = ?
1116                 ";
1117     $sth = $dbh->prepare($query);
1118     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1119     $query = "DELETE FROM reserves
1120                  WHERE  biblionumber     = ?
1121                     AND reservedate      = ?
1122                     AND borrowernumber   = ?
1123                 ";
1124     $sth = $dbh->prepare($query);
1125     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1126     
1127     # now fix the priority on the others (if the priority wasn't
1128     # already sorted!)....
1129     unless ( $priority == 0 ) {
1130         _FixPriority( $biblionumber, $borrowernumber );
1131     }
1132 }
1133
1134 =head2 ModReserveStatus
1135
1136   &ModReserveStatus($itemnumber, $newstatus);
1137
1138 Update the reserve status for the active (priority=0) reserve.
1139
1140 $itemnumber is the itemnumber the reserve is on
1141
1142 $newstatus is the new status.
1143
1144 =cut
1145
1146 sub ModReserveStatus {
1147
1148     #first : check if we have a reservation for this item .
1149     my ($itemnumber, $newstatus) = @_;
1150     my $dbh          = C4::Context->dbh;
1151     my $query = " UPDATE reserves
1152     SET    found=?,waitingdate = now()
1153     WHERE itemnumber=?
1154       AND found IS NULL
1155       AND priority = 0
1156     ";
1157     my $sth_set = $dbh->prepare($query);
1158     $sth_set->execute( $newstatus, $itemnumber );
1159
1160     if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1161       CartToShelf( $itemnumber );
1162     }
1163 }
1164
1165 =head2 ModReserveAffect
1166
1167   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1168
1169 This function affect an item and a status for a given reserve
1170 The itemnumber parameter is used to find the biblionumber.
1171 with the biblionumber & the borrowernumber, we can affect the itemnumber
1172 to the correct reserve.
1173
1174 if $transferToDo is not set, then the status is set to "Waiting" as well.
1175 otherwise, a transfer is on the way, and the end of the transfer will 
1176 take care of the waiting status
1177
1178 =cut
1179
1180 sub ModReserveAffect {
1181     my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1182     my $dbh = C4::Context->dbh;
1183
1184     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1185     # attached to $itemnumber
1186     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1187     $sth->execute($itemnumber);
1188     my ($biblionumber) = $sth->fetchrow;
1189
1190     # get request - need to find out if item is already
1191     # waiting in order to not send duplicate hold filled notifications
1192     my $request = GetReserveInfo($borrowernumber, $biblionumber);
1193     my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1194
1195     # If we affect a reserve that has to be transfered, don't set to Waiting
1196     my $query;
1197     if ($transferToDo) {
1198     $query = "
1199         UPDATE reserves
1200         SET    priority = 0,
1201                itemnumber = ?,
1202                found = 'T'
1203         WHERE borrowernumber = ?
1204           AND biblionumber = ?
1205     ";
1206     }
1207     else {
1208     # affect the reserve to Waiting as well.
1209     $query = "
1210         UPDATE reserves
1211         SET     priority = 0,
1212                 found = 'W',
1213                 waitingdate=now(),
1214                 itemnumber = ?
1215         WHERE borrowernumber = ?
1216           AND biblionumber = ?
1217     ";
1218     }
1219     $sth = $dbh->prepare($query);
1220     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1221     _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1222
1223     if ( C4::Context->preference("ReturnToShelvingCart") ) {
1224       CartToShelf( $itemnumber );
1225     }
1226
1227     return;
1228 }
1229
1230 =head2 ModReserveCancelAll
1231
1232   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1233
1234 function to cancel reserv,check other reserves, and transfer document if it's necessary
1235
1236 =cut
1237
1238 sub ModReserveCancelAll {
1239     my $messages;
1240     my $nextreservinfo;
1241     my ( $itemnumber, $borrowernumber ) = @_;
1242
1243     #step 1 : cancel the reservation
1244     my $CancelReserve = CancelReserve( undef, $itemnumber, $borrowernumber );
1245
1246     #step 2 launch the subroutine of the others reserves
1247     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1248
1249     return ( $messages, $nextreservinfo );
1250 }
1251
1252 =head2 ModReserveMinusPriority
1253
1254   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1255
1256 Reduce the values of queuded list     
1257
1258 =cut
1259
1260 sub ModReserveMinusPriority {
1261     my ( $itemnumber, $borrowernumber, $biblionumber ) = @_;
1262
1263     #first step update the value of the first person on reserv
1264     my $dbh   = C4::Context->dbh;
1265     my $query = "
1266         UPDATE reserves
1267         SET    priority = 0 , itemnumber = ? 
1268         WHERE  borrowernumber=?
1269           AND  biblionumber=?
1270     ";
1271     my $sth_upd = $dbh->prepare($query);
1272     $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber );
1273     # second step update all others reservs
1274     _FixPriority($biblionumber, $borrowernumber, '0');
1275 }
1276
1277 =head2 GetReserveInfo
1278
1279   &GetReserveInfo($borrowernumber,$biblionumber);
1280
1281 Get item and borrower details for a current hold.
1282 Current implementation this query should have a single result.
1283
1284 =cut
1285
1286 sub GetReserveInfo {
1287         my ( $borrowernumber, $biblionumber ) = @_;
1288     my $dbh = C4::Context->dbh;
1289         my $strsth="SELECT 
1290                        reservedate, 
1291                        reservenotes, 
1292                        reserves.borrowernumber,
1293                                    reserves.biblionumber, 
1294                                    reserves.branchcode,
1295                                    reserves.waitingdate,
1296                                    notificationdate, 
1297                                    reminderdate, 
1298                                    priority, 
1299                                    found,
1300                                    firstname, 
1301                                    surname, 
1302                                    phone, 
1303                                    email, 
1304                                    address, 
1305                                    address2,
1306                                    cardnumber, 
1307                                    city, 
1308                                    zipcode,
1309                                    biblio.title, 
1310                                    biblio.author,
1311                                    items.holdingbranch, 
1312                                    items.itemcallnumber, 
1313                                    items.itemnumber,
1314                                    items.location, 
1315                                    barcode, 
1316                                    notes
1317                         FROM reserves 
1318                          LEFT JOIN items USING(itemnumber) 
1319                      LEFT JOIN borrowers USING(borrowernumber)
1320                      LEFT JOIN biblio ON  (reserves.biblionumber=biblio.biblionumber) 
1321                         WHERE 
1322                                 reserves.borrowernumber=?
1323                                 AND reserves.biblionumber=?";
1324         my $sth = $dbh->prepare($strsth); 
1325         $sth->execute($borrowernumber,$biblionumber);
1326
1327         my $data = $sth->fetchrow_hashref;
1328         return $data;
1329
1330 }
1331
1332 =head2 IsAvailableForItemLevelRequest
1333
1334   my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1335
1336 Checks whether a given item record is available for an
1337 item-level hold request.  An item is available if
1338
1339 * it is not lost AND 
1340 * it is not damaged AND 
1341 * it is not withdrawn AND 
1342 * does not have a not for loan value > 0
1343
1344 Whether or not the item is currently on loan is 
1345 also checked - if the AllowOnShelfHolds system preference
1346 is ON, an item can be requested even if it is currently
1347 on loan to somebody else.  If the system preference
1348 is OFF, an item that is currently checked out cannot
1349 be the target of an item-level hold request.
1350
1351 Note that IsAvailableForItemLevelRequest() does not
1352 check if the staff operator is authorized to place
1353 a request on the item - in particular,
1354 this routine does not check IndependantBranches
1355 and canreservefromotherbranches.
1356
1357 =cut
1358
1359 sub IsAvailableForItemLevelRequest {
1360     my $itemnumber = shift;
1361    
1362     my $item = GetItem($itemnumber);
1363
1364     # must check the notforloan setting of the itemtype
1365     # FIXME - a lot of places in the code do this
1366     #         or something similar - need to be
1367     #         consolidated
1368     my $dbh = C4::Context->dbh;
1369     my $notforloan_query;
1370     if (C4::Context->preference('item-level_itypes')) {
1371         $notforloan_query = "SELECT itemtypes.notforloan
1372                              FROM items
1373                              JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1374                              WHERE itemnumber = ?";
1375     } else {
1376         $notforloan_query = "SELECT itemtypes.notforloan
1377                              FROM items
1378                              JOIN biblioitems USING (biblioitemnumber)
1379                              JOIN itemtypes USING (itemtype)
1380                              WHERE itemnumber = ?";
1381     }
1382     my $sth = $dbh->prepare($notforloan_query);
1383     $sth->execute($itemnumber);
1384     my $notforloan_per_itemtype = 0;
1385     if (my ($notforloan) = $sth->fetchrow_array) {
1386         $notforloan_per_itemtype = 1 if $notforloan;
1387     }
1388
1389     my $available_per_item = 1;
1390     $available_per_item = 0 if $item->{itemlost} or
1391                                ( $item->{notforloan} > 0 ) or
1392                                ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1393                                $item->{wthdrawn} or
1394                                $notforloan_per_itemtype;
1395
1396
1397     if (C4::Context->preference('AllowOnShelfHolds')) {
1398         return $available_per_item;
1399     } else {
1400         return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "W")); 
1401     }
1402 }
1403
1404 =head2 AlterPriority
1405
1406   AlterPriority( $where, $borrowernumber, $biblionumber, $reservedate );
1407
1408 This function changes a reserve's priority up, down, to the top, or to the bottom.
1409 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1410
1411 =cut
1412
1413 sub AlterPriority {
1414     my ( $where, $borrowernumber, $biblionumber ) = @_;
1415
1416     my $dbh = C4::Context->dbh;
1417
1418     ## Find this reserve
1419     my $sth = $dbh->prepare('SELECT * FROM reserves WHERE biblionumber = ? AND borrowernumber = ? AND cancellationdate IS NULL');
1420     $sth->execute( $biblionumber, $borrowernumber );
1421     my $reserve = $sth->fetchrow_hashref();
1422     $sth->finish();
1423
1424     if ( $where eq 'up' || $where eq 'down' ) {
1425     
1426       my $priority = $reserve->{'priority'};        
1427       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1428       _FixPriority( $biblionumber, $borrowernumber, $priority )
1429
1430     } elsif ( $where eq 'top' ) {
1431
1432       _FixPriority( $biblionumber, $borrowernumber, '1' )
1433
1434     } elsif ( $where eq 'bottom' ) {
1435
1436       _FixPriority( $biblionumber, $borrowernumber, '999999' )
1437
1438     }
1439 }
1440
1441 =head2 ToggleLowestPriority
1442
1443   ToggleLowestPriority( $borrowernumber, $biblionumber );
1444
1445 This function sets the lowestPriority field to true if is false, and false if it is true.
1446
1447 =cut
1448
1449 sub ToggleLowestPriority {
1450     my ( $borrowernumber, $biblionumber ) = @_;
1451
1452     my $dbh = C4::Context->dbh;
1453
1454     my $sth = $dbh->prepare(
1455         "UPDATE reserves SET lowestPriority = NOT lowestPriority
1456          WHERE biblionumber = ?
1457          AND borrowernumber = ?"
1458     );
1459     $sth->execute(
1460         $biblionumber,
1461         $borrowernumber,
1462     );
1463     $sth->finish;
1464     
1465     _FixPriority( $biblionumber, $borrowernumber, '999999' );
1466 }
1467
1468 =head2 _FixPriority
1469
1470   &_FixPriority($biblio,$borrowernumber,$rank,$ignoreSetLowestRank);
1471
1472 Only used internally (so don't export it)
1473 Changed how this functions works #
1474 Now just gets an array of reserves in the rank order and updates them with
1475 the array index (+1 as array starts from 0)
1476 and if $rank is supplied will splice item from the array and splice it back in again
1477 in new priority rank
1478
1479 =cut 
1480
1481 sub _FixPriority {
1482     my ( $biblio, $borrowernumber, $rank, $ignoreSetLowestRank ) = @_;
1483     my $dbh = C4::Context->dbh;
1484      if ( $rank eq "del" ) {
1485          CancelReserve( $biblio, undef, $borrowernumber );
1486      }
1487     if ( $rank eq "W" || $rank eq "0" ) {
1488
1489         # make sure priority for waiting or in-transit items is 0
1490         my $query = qq/
1491             UPDATE reserves
1492             SET    priority = 0
1493             WHERE biblionumber = ?
1494               AND borrowernumber = ?
1495               AND found IN ('W', 'T')
1496         /;
1497         my $sth = $dbh->prepare($query);
1498         $sth->execute( $biblio, $borrowernumber );
1499     }
1500     my @priority;
1501     my @reservedates;
1502
1503     # get whats left
1504 # FIXME adding a new security in returned elements for changing priority,
1505 # now, we don't care anymore any reservations with itemnumber linked (suppose a waiting reserve)
1506         # This is wrong a waiting reserve has W set
1507         # The assumption that having an itemnumber set means waiting is wrong and should be corrected any place it occurs
1508     my $query = qq/
1509         SELECT borrowernumber, reservedate, constrainttype
1510         FROM   reserves
1511         WHERE  biblionumber   = ?
1512           AND  ((found <> 'W' AND found <> 'T') or found is NULL)
1513         ORDER BY priority ASC
1514     /;
1515     my $sth = $dbh->prepare($query);
1516     $sth->execute($biblio);
1517     while ( my $line = $sth->fetchrow_hashref ) {
1518         push( @reservedates, $line );
1519         push( @priority,     $line );
1520     }
1521
1522     # To find the matching index
1523     my $i;
1524     my $key = -1;    # to allow for 0 to be a valid result
1525     for ( $i = 0 ; $i < @priority ; $i++ ) {
1526         if ( $borrowernumber == $priority[$i]->{'borrowernumber'} ) {
1527             $key = $i;    # save the index
1528             last;
1529         }
1530     }
1531
1532     # if index exists in array then move it to new position
1533     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1534         my $new_rank = $rank -
1535           1;    # $new_rank is what you want the new index to be in the array
1536         my $moving_item = splice( @priority, $key, 1 );
1537         splice( @priority, $new_rank, 0, $moving_item );
1538     }
1539
1540     # now fix the priority on those that are left....
1541     $query = "
1542             UPDATE reserves
1543             SET    priority = ?
1544                 WHERE  biblionumber = ?
1545                  AND borrowernumber   = ?
1546                  AND reservedate = ?
1547          AND found IS NULL
1548     ";
1549     $sth = $dbh->prepare($query);
1550     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1551         $sth->execute(
1552             $j + 1, $biblio,
1553             $priority[$j]->{'borrowernumber'},
1554             $priority[$j]->{'reservedate'}
1555         );
1556         $sth->finish;
1557     }
1558     
1559     $sth = $dbh->prepare( "SELECT borrowernumber FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1560     $sth->execute();
1561     
1562     unless ( $ignoreSetLowestRank ) {
1563       while ( my $res = $sth->fetchrow_hashref() ) {
1564         _FixPriority( $biblio, $res->{'borrowernumber'}, '999999', 1 );
1565       }
1566     }
1567 }
1568
1569 =head2 _Findgroupreserve
1570
1571   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1572
1573 Looks for an item-specific match first, then for a title-level match, returning the
1574 first match found.  If neither, then we look for a 3rd kind of match based on
1575 reserve constraints.
1576
1577 TODO: add more explanation about reserve constraints
1578
1579 C<&_Findgroupreserve> returns :
1580 C<@results> is an array of references-to-hash whose keys are mostly
1581 fields from the reserves table of the Koha database, plus
1582 C<biblioitemnumber>.
1583
1584 =cut
1585
1586 sub _Findgroupreserve {
1587     my ( $bibitem, $biblio, $itemnumber ) = @_;
1588     my $dbh   = C4::Context->dbh;
1589
1590     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1591     # check for exact targetted match
1592     my $item_level_target_query = qq/
1593         SELECT reserves.biblionumber        AS biblionumber,
1594                reserves.borrowernumber      AS borrowernumber,
1595                reserves.reservedate         AS reservedate,
1596                reserves.branchcode          AS branchcode,
1597                reserves.cancellationdate    AS cancellationdate,
1598                reserves.found               AS found,
1599                reserves.reservenotes        AS reservenotes,
1600                reserves.priority            AS priority,
1601                reserves.timestamp           AS timestamp,
1602                biblioitems.biblioitemnumber AS biblioitemnumber,
1603                reserves.itemnumber          AS itemnumber
1604         FROM reserves
1605         JOIN biblioitems USING (biblionumber)
1606         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1607         WHERE found IS NULL
1608         AND priority > 0
1609         AND item_level_request = 1
1610         AND itemnumber = ?
1611         AND reservedate <= CURRENT_DATE()
1612     /;
1613     my $sth = $dbh->prepare($item_level_target_query);
1614     $sth->execute($itemnumber);
1615     my @results;
1616     if ( my $data = $sth->fetchrow_hashref ) {
1617         push( @results, $data );
1618     }
1619     return @results if @results;
1620     
1621     # check for title-level targetted match
1622     my $title_level_target_query = qq/
1623         SELECT reserves.biblionumber        AS biblionumber,
1624                reserves.borrowernumber      AS borrowernumber,
1625                reserves.reservedate         AS reservedate,
1626                reserves.branchcode          AS branchcode,
1627                reserves.cancellationdate    AS cancellationdate,
1628                reserves.found               AS found,
1629                reserves.reservenotes        AS reservenotes,
1630                reserves.priority            AS priority,
1631                reserves.timestamp           AS timestamp,
1632                biblioitems.biblioitemnumber AS biblioitemnumber,
1633                reserves.itemnumber          AS itemnumber
1634         FROM reserves
1635         JOIN biblioitems USING (biblionumber)
1636         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1637         WHERE found IS NULL
1638         AND priority > 0
1639         AND item_level_request = 0
1640         AND hold_fill_targets.itemnumber = ?
1641         AND reservedate <= CURRENT_DATE()
1642     /;
1643     $sth = $dbh->prepare($title_level_target_query);
1644     $sth->execute($itemnumber);
1645     @results = ();
1646     if ( my $data = $sth->fetchrow_hashref ) {
1647         push( @results, $data );
1648     }
1649     return @results if @results;
1650
1651     my $query = qq/
1652         SELECT reserves.biblionumber               AS biblionumber,
1653                reserves.borrowernumber             AS borrowernumber,
1654                reserves.reservedate                AS reservedate,
1655                reserves.waitingdate                AS waitingdate,
1656                reserves.branchcode                 AS branchcode,
1657                reserves.cancellationdate           AS cancellationdate,
1658                reserves.found                      AS found,
1659                reserves.reservenotes               AS reservenotes,
1660                reserves.priority                   AS priority,
1661                reserves.timestamp                  AS timestamp,
1662                reserveconstraints.biblioitemnumber AS biblioitemnumber,
1663                reserves.itemnumber                 AS itemnumber
1664         FROM reserves
1665           LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1666         WHERE reserves.biblionumber = ?
1667           AND ( ( reserveconstraints.biblioitemnumber = ?
1668           AND reserves.borrowernumber = reserveconstraints.borrowernumber
1669           AND reserves.reservedate    = reserveconstraints.reservedate )
1670           OR  reserves.constrainttype='a' )
1671           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1672           AND reserves.reservedate <= CURRENT_DATE()
1673     /;
1674     $sth = $dbh->prepare($query);
1675     $sth->execute( $biblio, $bibitem, $itemnumber );
1676     @results = ();
1677     while ( my $data = $sth->fetchrow_hashref ) {
1678         push( @results, $data );
1679     }
1680     return @results;
1681 }
1682
1683 =head2 _koha_notify_reserve
1684
1685   _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1686
1687 Sends a notification to the patron that their hold has been filled (through
1688 ModReserveAffect, _not_ ModReserveFill)
1689
1690 =cut
1691
1692 sub _koha_notify_reserve {
1693     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1694
1695     my $dbh = C4::Context->dbh;
1696     my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1697     
1698     # Try to get the borrower's email address
1699     my $to_address;
1700     my $which_address = C4::Context->preference('AutoEmailPrimaryAddress');
1701     # If the system preference is set to 'first valid' (value == OFF), look up email address
1702     if ($which_address eq 'OFF') {
1703         $to_address = C4::Members::GetFirstValidEmailAddress( $borrowernumber );
1704     } else {
1705         $to_address = $borrower->{$which_address};
1706     }
1707     
1708     my $letter_code;
1709     my $print_mode = 0;
1710     my $messagingprefs;
1711     if ( $to_address || $borrower->{'smsalertnumber'} ) {
1712         $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } );
1713
1714         return if ( !defined( $messagingprefs->{'letter_code'} ) );
1715         $letter_code = $messagingprefs->{'letter_code'};
1716     } else {
1717         $letter_code = 'HOLD_PRINT';
1718         $print_mode = 1;
1719     }
1720
1721     my $sth = $dbh->prepare("
1722         SELECT *
1723         FROM   reserves
1724         WHERE  borrowernumber = ?
1725             AND biblionumber = ?
1726     ");
1727     $sth->execute( $borrowernumber, $biblionumber );
1728     my $reserve = $sth->fetchrow_hashref;
1729     my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1730
1731     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1732
1733     my $letter = getletter( 'reserves', $letter_code );
1734     die "Could not find a letter called '$letter_code' in the 'reserves' module" unless( $letter );
1735
1736     C4::Letters::parseletter( $letter, 'branches', $reserve->{'branchcode'} );
1737     C4::Letters::parseletter( $letter, 'borrowers', $borrowernumber );
1738     C4::Letters::parseletter( $letter, 'biblio', $biblionumber );
1739     C4::Letters::parseletter( $letter, 'reserves', $borrowernumber, $biblionumber );
1740
1741     if ( $reserve->{'itemnumber'} ) {
1742         C4::Letters::parseletter( $letter, 'items', $reserve->{'itemnumber'} );
1743     }
1744     my $today = C4::Dates->new()->output();
1745     $letter->{'title'} =~ s/<<today>>/$today/g;
1746     $letter->{'content'} =~ s/<<today>>/$today/g;
1747     $letter->{'content'} =~ s/<<[a-z0-9_]+\.[a-z0-9]+>>//g; #remove any stragglers
1748
1749     if ( $print_mode ) {
1750         C4::Letters::EnqueueLetter( {
1751             letter => $letter,
1752             borrowernumber => $borrowernumber,
1753             message_transport_type => 'print',
1754         } );
1755         
1756         return;
1757     }
1758
1759     if ( grep { $_ eq 'email' } @{$messagingprefs->{transports}} ) {
1760         # aka, 'email' in ->{'transports'}
1761         C4::Letters::EnqueueLetter(
1762             {   letter                 => $letter,
1763                 borrowernumber         => $borrowernumber,
1764                 message_transport_type => 'email',
1765                 from_address           => $admin_email_address,
1766             }
1767         );
1768     }
1769
1770     if ( grep { $_ eq 'sms' } @{$messagingprefs->{transports}} ) {
1771         C4::Letters::EnqueueLetter(
1772             {   letter                 => $letter,
1773                 borrowernumber         => $borrowernumber,
1774                 message_transport_type => 'sms',
1775             }
1776         );
1777     }
1778 }
1779
1780 =head2 _ShiftPriorityByDateAndPriority
1781
1782   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1783
1784 This increments the priority of all reserves after the one
1785 with either the lowest date after C<$reservedate>
1786 or the lowest priority after C<$priority>.
1787
1788 It effectively makes room for a new reserve to be inserted with a certain
1789 priority, which is returned.
1790
1791 This is most useful when the reservedate can be set by the user.  It allows
1792 the new reserve to be placed before other reserves that have a later
1793 reservedate.  Since priority also is set by the form in reserves/request.pl
1794 the sub accounts for that too.
1795
1796 =cut
1797
1798 sub _ShiftPriorityByDateAndPriority {
1799     my ( $biblio, $resdate, $new_priority ) = @_;
1800
1801     my $dbh = C4::Context->dbh;
1802     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1803     my $sth = $dbh->prepare( $query );
1804     $sth->execute( $biblio, $resdate, $new_priority );
1805     my $min_priority = $sth->fetchrow;
1806     # if no such matches are found, $new_priority remains as original value
1807     $new_priority = $min_priority if ( $min_priority );
1808
1809     # Shift the priority up by one; works in conjunction with the next SQL statement
1810     $query = "UPDATE reserves
1811               SET priority = priority+1
1812               WHERE biblionumber = ?
1813               AND borrowernumber = ?
1814               AND reservedate = ?
1815               AND found IS NULL";
1816     my $sth_update = $dbh->prepare( $query );
1817
1818     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1819     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1820     $sth = $dbh->prepare( $query );
1821     $sth->execute( $new_priority, $biblio );
1822     while ( my $row = $sth->fetchrow_hashref ) {
1823         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1824     }
1825
1826     return $new_priority;  # so the caller knows what priority they wind up receiving
1827 }
1828
1829 =head2 MergeHolds
1830
1831   MergeHolds($dbh,$to_biblio, $from_biblio);
1832
1833 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1834
1835 =cut
1836
1837 sub MergeHolds {
1838     my ( $dbh, $to_biblio, $from_biblio ) = @_;
1839     my $sth = $dbh->prepare(
1840         "SELECT count(*) as reservenumber FROM reserves WHERE biblionumber = ?"
1841     );
1842     $sth->execute($from_biblio);
1843     if ( my $data = $sth->fetchrow_hashref() ) {
1844
1845         # holds exist on old record, if not we don't need to do anything
1846         $sth = $dbh->prepare(
1847             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1848         $sth->execute( $to_biblio, $from_biblio );
1849
1850         # Reorder by date
1851         # don't reorder those already waiting
1852
1853         $sth = $dbh->prepare(
1854 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1855         );
1856         my $upd_sth = $dbh->prepare(
1857 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1858         AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
1859         );
1860         $sth->execute( $to_biblio, 'W', 'T' );
1861         my $priority = 1;
1862         while ( my $reserve = $sth->fetchrow_hashref() ) {
1863             $upd_sth->execute(
1864                 $priority,                    $to_biblio,
1865                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1866                 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
1867             );
1868             $priority++;
1869         }
1870     }
1871 }
1872
1873
1874 =head1 AUTHOR
1875
1876 Koha Development Team <http://koha-community.org/>
1877
1878 =cut
1879
1880 1;