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