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