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