Bug 14377: [QA Follow-up] Correct typo and comment
[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                    found            = Null,
1077                    priority         = 0
1078             WHERE  reserve_id = ?
1079         ";
1080         my $sth = $dbh->prepare($query);
1081         $sth->execute( $reserve_id );
1082
1083         $query = "
1084             INSERT INTO old_reserves
1085             SELECT * FROM reserves
1086             WHERE  reserve_id = ?
1087         ";
1088         $sth = $dbh->prepare($query);
1089         $sth->execute( $reserve_id );
1090
1091         $query = "
1092             DELETE FROM reserves
1093             WHERE  reserve_id = ?
1094         ";
1095         $sth = $dbh->prepare($query);
1096         $sth->execute( $reserve_id );
1097
1098         # now fix the priority on the others....
1099         _FixPriority({ biblionumber => $reserve->{biblionumber} });
1100
1101         # and, if desired, charge a cancel fee
1102         my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
1103         if ( $charge && $params->{'charge_cancel_fee'} ) {
1104             manualinvoice($reserve->{'borrowernumber'}, $reserve->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
1105         }
1106     }
1107
1108     return $reserve;
1109 }
1110
1111 =head2 ModReserve
1112
1113   ModReserve({ rank => $rank,
1114                reserve_id => $reserve_id,
1115                branchcode => $branchcode
1116                [, itemnumber => $itemnumber ]
1117                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1118               });
1119
1120 Change a hold request's priority or cancel it.
1121
1122 C<$rank> specifies the effect of the change.  If C<$rank>
1123 is 'W' or 'n', nothing happens.  This corresponds to leaving a
1124 request alone when changing its priority in the holds queue
1125 for a bib.
1126
1127 If C<$rank> is 'del', the hold request is cancelled.
1128
1129 If C<$rank> is an integer greater than zero, the priority of
1130 the request is set to that value.  Since priority != 0 means
1131 that the item is not waiting on the hold shelf, setting the
1132 priority to a non-zero value also sets the request's found
1133 status and waiting date to NULL.
1134
1135 The optional C<$itemnumber> parameter is used only when
1136 C<$rank> is a non-zero integer; if supplied, the itemnumber
1137 of the hold request is set accordingly; if omitted, the itemnumber
1138 is cleared.
1139
1140 B<FIXME:> Note that the forgoing can have the effect of causing
1141 item-level hold requests to turn into title-level requests.  This
1142 will be fixed once reserves has separate columns for requested
1143 itemnumber and supplying itemnumber.
1144
1145 =cut
1146
1147 sub ModReserve {
1148     my ( $params ) = @_;
1149
1150     my $rank = $params->{'rank'};
1151     my $reserve_id = $params->{'reserve_id'};
1152     my $branchcode = $params->{'branchcode'};
1153     my $itemnumber = $params->{'itemnumber'};
1154     my $suspend_until = $params->{'suspend_until'};
1155     my $borrowernumber = $params->{'borrowernumber'};
1156     my $biblionumber = $params->{'biblionumber'};
1157
1158     return if $rank eq "W";
1159     return if $rank eq "n";
1160
1161     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1162     $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1163
1164     my $dbh = C4::Context->dbh;
1165     if ( $rank eq "del" ) {
1166         CancelReserve({ reserve_id => $reserve_id });
1167     }
1168     elsif ($rank =~ /^\d+/ and $rank > 0) {
1169         my $hold = Koha::Holds->find($reserve_id);
1170
1171         $hold->set(
1172             {
1173                 priority    => $rank,
1174                 branchcode  => $branchcode,
1175                 itemnumber  => $itemnumber,
1176                 found       => undef,
1177                 waitingdate => undef
1178             }
1179         )->store();
1180
1181         if ( defined( $suspend_until ) ) {
1182             if ( $suspend_until ) {
1183                 $suspend_until = eval { dt_from_string( $suspend_until ) };
1184                 $hold->suspend_hold( $suspend_until );
1185             } else {
1186                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1187                 # If the hold is not suspended, this does nothing.
1188                 $hold->set( { suspend_until => undef } )->store();
1189             }
1190         }
1191
1192         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1193     }
1194 }
1195
1196 =head2 ModReserveFill
1197
1198   &ModReserveFill($reserve);
1199
1200 Fill a reserve. If I understand this correctly, this means that the
1201 reserved book has been found and given to the patron who reserved it.
1202
1203 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1204 whose keys are fields from the reserves table in the Koha database.
1205
1206 =cut
1207
1208 sub ModReserveFill {
1209     my ($res) = @_;
1210     my $dbh = C4::Context->dbh;
1211     # fill in a reserve record....
1212     my $reserve_id = $res->{'reserve_id'};
1213     my $biblionumber = $res->{'biblionumber'};
1214     my $borrowernumber    = $res->{'borrowernumber'};
1215     my $resdate = $res->{'reservedate'};
1216
1217     # get the priority on this record....
1218     my $priority;
1219     my $query = "SELECT priority
1220                  FROM   reserves
1221                  WHERE  biblionumber   = ?
1222                   AND   borrowernumber = ?
1223                   AND   reservedate    = ?";
1224     my $sth = $dbh->prepare($query);
1225     $sth->execute( $biblionumber, $borrowernumber, $resdate );
1226     ($priority) = $sth->fetchrow_array;
1227
1228     # update the database...
1229     $query = "UPDATE reserves
1230                   SET    found            = 'F',
1231                          priority         = 0
1232                  WHERE  biblionumber     = ?
1233                     AND reservedate      = ?
1234                     AND borrowernumber   = ?
1235                 ";
1236     $sth = $dbh->prepare($query);
1237     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1238
1239     # move to old_reserves
1240     $query = "INSERT INTO old_reserves
1241                  SELECT * FROM reserves
1242                  WHERE  biblionumber     = ?
1243                     AND reservedate      = ?
1244                     AND borrowernumber   = ?
1245                 ";
1246     $sth = $dbh->prepare($query);
1247     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1248     $query = "DELETE FROM reserves
1249                  WHERE  biblionumber     = ?
1250                     AND reservedate      = ?
1251                     AND borrowernumber   = ?
1252                 ";
1253     $sth = $dbh->prepare($query);
1254     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1255
1256     # now fix the priority on the others (if the priority wasn't
1257     # already sorted!)....
1258     unless ( $priority == 0 ) {
1259         _FixPriority({ reserve_id => $reserve_id, biblionumber => $biblionumber });
1260     }
1261 }
1262
1263 =head2 ModReserveStatus
1264
1265   &ModReserveStatus($itemnumber, $newstatus);
1266
1267 Update the reserve status for the active (priority=0) reserve.
1268
1269 $itemnumber is the itemnumber the reserve is on
1270
1271 $newstatus is the new status.
1272
1273 =cut
1274
1275 sub ModReserveStatus {
1276
1277     #first : check if we have a reservation for this item .
1278     my ($itemnumber, $newstatus) = @_;
1279     my $dbh = C4::Context->dbh;
1280
1281     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1282     my $sth_set = $dbh->prepare($query);
1283     $sth_set->execute( $newstatus, $itemnumber );
1284
1285     if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1286       CartToShelf( $itemnumber );
1287     }
1288 }
1289
1290 =head2 ModReserveAffect
1291
1292   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1293
1294 This function affect an item and a status for a given reserve
1295 The itemnumber parameter is used to find the biblionumber.
1296 with the biblionumber & the borrowernumber, we can affect the itemnumber
1297 to the correct reserve.
1298
1299 if $transferToDo is not set, then the status is set to "Waiting" as well.
1300 otherwise, a transfer is on the way, and the end of the transfer will
1301 take care of the waiting status
1302
1303 =cut
1304
1305 sub ModReserveAffect {
1306     my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1307     my $dbh = C4::Context->dbh;
1308
1309     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1310     # attached to $itemnumber
1311     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1312     $sth->execute($itemnumber);
1313     my ($biblionumber) = $sth->fetchrow;
1314
1315     # get request - need to find out if item is already
1316     # waiting in order to not send duplicate hold filled notifications
1317     my $reserve_id = GetReserveId({
1318         borrowernumber => $borrowernumber,
1319         biblionumber   => $biblionumber,
1320     });
1321     return unless defined $reserve_id;
1322     my $request = GetReserveInfo($reserve_id);
1323     my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1324
1325     # If we affect a reserve that has to be transferred, don't set to Waiting
1326     my $query;
1327     if ($transferToDo) {
1328     $query = "
1329         UPDATE reserves
1330         SET    priority = 0,
1331                itemnumber = ?,
1332                found = 'T'
1333         WHERE borrowernumber = ?
1334           AND biblionumber = ?
1335     ";
1336     }
1337     else {
1338     # affect the reserve to Waiting as well.
1339         $query = "
1340             UPDATE reserves
1341             SET     priority = 0,
1342                     found = 'W',
1343                     waitingdate = NOW(),
1344                     itemnumber = ?
1345             WHERE borrowernumber = ?
1346               AND biblionumber = ?
1347         ";
1348     }
1349     $sth = $dbh->prepare($query);
1350     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1351     _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1352     _FixPriority( { biblionumber => $biblionumber } );
1353     if ( C4::Context->preference("ReturnToShelvingCart") ) {
1354       CartToShelf( $itemnumber );
1355     }
1356
1357     return;
1358 }
1359
1360 =head2 ModReserveCancelAll
1361
1362   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1363
1364 function to cancel reserv,check other reserves, and transfer document if it's necessary
1365
1366 =cut
1367
1368 sub ModReserveCancelAll {
1369     my $messages;
1370     my $nextreservinfo;
1371     my ( $itemnumber, $borrowernumber ) = @_;
1372
1373     #step 1 : cancel the reservation
1374     my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1375
1376     #step 2 launch the subroutine of the others reserves
1377     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1378
1379     return ( $messages, $nextreservinfo );
1380 }
1381
1382 =head2 ModReserveMinusPriority
1383
1384   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1385
1386 Reduce the values of queued list
1387
1388 =cut
1389
1390 sub ModReserveMinusPriority {
1391     my ( $itemnumber, $reserve_id ) = @_;
1392
1393     #first step update the value of the first person on reserv
1394     my $dbh   = C4::Context->dbh;
1395     my $query = "
1396         UPDATE reserves
1397         SET    priority = 0 , itemnumber = ? 
1398         WHERE  reserve_id = ?
1399     ";
1400     my $sth_upd = $dbh->prepare($query);
1401     $sth_upd->execute( $itemnumber, $reserve_id );
1402     # second step update all others reserves
1403     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1404 }
1405
1406 =head2 GetReserveInfo
1407
1408   &GetReserveInfo($reserve_id);
1409
1410 Get item and borrower details for a current hold.
1411 Current implementation this query should have a single result.
1412
1413 =cut
1414
1415 sub GetReserveInfo {
1416     my ( $reserve_id ) = @_;
1417     my $dbh = C4::Context->dbh;
1418     my $strsth="SELECT
1419                    reserve_id,
1420                    reservedate,
1421                    reservenotes,
1422                    reserves.borrowernumber,
1423                    reserves.biblionumber,
1424                    reserves.branchcode,
1425                    reserves.waitingdate,
1426                    notificationdate,
1427                    reminderdate,
1428                    priority,
1429                    found,
1430                    firstname,
1431                    surname,
1432                    phone,
1433                    email,
1434                    address,
1435                    address2,
1436                    cardnumber,
1437                    city,
1438                    zipcode,
1439                    biblio.title,
1440                    biblio.author,
1441                    items.holdingbranch,
1442                    items.itemcallnumber,
1443                    items.itemnumber,
1444                    items.location,
1445                    barcode,
1446                    notes
1447                 FROM reserves
1448                 LEFT JOIN items USING(itemnumber)
1449                 LEFT JOIN borrowers USING(borrowernumber)
1450                 LEFT JOIN biblio ON  (reserves.biblionumber=biblio.biblionumber)
1451                 WHERE reserves.reserve_id = ?";
1452     my $sth = $dbh->prepare($strsth);
1453     $sth->execute($reserve_id);
1454
1455     my $data = $sth->fetchrow_hashref;
1456     return $data;
1457 }
1458
1459 =head2 IsAvailableForItemLevelRequest
1460
1461   my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1462
1463 Checks whether a given item record is available for an
1464 item-level hold request.  An item is available if
1465
1466 * it is not lost AND
1467 * it is not damaged AND
1468 * it is not withdrawn AND
1469 * does not have a not for loan value > 0
1470
1471 Need to check the issuingrules onshelfholds column,
1472 if this is set items on the shelf can be placed on hold
1473
1474 Note that IsAvailableForItemLevelRequest() does not
1475 check if the staff operator is authorized to place
1476 a request on the item - in particular,
1477 this routine does not check IndependentBranches
1478 and canreservefromotherbranches.
1479
1480 =cut
1481
1482 sub IsAvailableForItemLevelRequest {
1483     my $item = shift;
1484     my $borrower = shift;
1485
1486     my $dbh = C4::Context->dbh;
1487     # must check the notforloan setting of the itemtype
1488     # FIXME - a lot of places in the code do this
1489     #         or something similar - need to be
1490     #         consolidated
1491     my $itype = _get_itype($item);
1492     my $notforloan_per_itemtype
1493       = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1494                               undef, $itype);
1495
1496     return 0 if
1497         $notforloan_per_itemtype ||
1498         $item->{itemlost}        ||
1499         $item->{notforloan} > 0  ||
1500         $item->{withdrawn}        ||
1501         ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1502
1503     my $on_shelf_holds = _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1504
1505     if ( $on_shelf_holds == 1 ) {
1506         return 1;
1507     } elsif ( $on_shelf_holds == 2 ) {
1508         my @items =
1509           Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1510
1511         my $any_available = 0;
1512
1513         foreach my $i (@items) {
1514             $any_available = 1
1515               unless $i->itemlost
1516               || $i->{notforloan} > 0
1517               || $i->withdrawn
1518               || $i->onloan
1519               || IsItemOnHoldAndFound( $i->id )
1520               || ( $i->damaged
1521                 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1522               || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan;
1523         }
1524
1525         return $any_available ? 0 : 1;
1526     }
1527
1528     return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting";
1529 }
1530
1531 =head2 OnShelfHoldsAllowed
1532
1533   OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode);
1534
1535 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf
1536 holds are allowed, returns true if so.
1537
1538 =cut
1539
1540 sub OnShelfHoldsAllowed {
1541     my ($item, $borrower) = @_;
1542
1543     my $itype = _get_itype($item);
1544     return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1545 }
1546
1547 sub _get_itype {
1548     my $item = shift;
1549
1550     my $itype;
1551     if (C4::Context->preference('item-level_itypes')) {
1552         # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1553         # When GetItem is fixed, we can remove this
1554         $itype = $item->{itype};
1555     }
1556     else {
1557         # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1558         # So if we already have a biblioitems join when calling this function,
1559         # we don't need to access the database again
1560         $itype = $item->{itemtype};
1561     }
1562     unless ($itype) {
1563         my $dbh = C4::Context->dbh;
1564         my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1565         my $sth = $dbh->prepare($query);
1566         $sth->execute($item->{biblioitemnumber});
1567         if (my $data = $sth->fetchrow_hashref()){
1568             $itype = $data->{itemtype};
1569         }
1570     }
1571     return $itype;
1572 }
1573
1574 sub _OnShelfHoldsAllowed {
1575     my ($itype,$borrowercategory,$branchcode) = @_;
1576
1577     my $rule = C4::Circulation::GetIssuingRule($borrowercategory, $itype, $branchcode);
1578     return $rule->{onshelfholds};
1579 }
1580
1581 =head2 AlterPriority
1582
1583   AlterPriority( $where, $reserve_id );
1584
1585 This function changes a reserve's priority up, down, to the top, or to the bottom.
1586 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1587
1588 =cut
1589
1590 sub AlterPriority {
1591     my ( $where, $reserve_id ) = @_;
1592
1593     my $dbh = C4::Context->dbh;
1594
1595     my $reserve = GetReserve( $reserve_id );
1596
1597     if ( $reserve->{cancellationdate} ) {
1598         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1599         return;
1600     }
1601
1602     if ( $where eq 'up' || $where eq 'down' ) {
1603
1604       my $priority = $reserve->{'priority'};
1605       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1606       _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1607
1608     } elsif ( $where eq 'top' ) {
1609
1610       _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1611
1612     } elsif ( $where eq 'bottom' ) {
1613
1614       _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1615
1616     }
1617 }
1618
1619 =head2 ToggleLowestPriority
1620
1621   ToggleLowestPriority( $borrowernumber, $biblionumber );
1622
1623 This function sets the lowestPriority field to true if is false, and false if it is true.
1624
1625 =cut
1626
1627 sub ToggleLowestPriority {
1628     my ( $reserve_id ) = @_;
1629
1630     my $dbh = C4::Context->dbh;
1631
1632     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1633     $sth->execute( $reserve_id );
1634     
1635     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1636 }
1637
1638 =head2 ToggleSuspend
1639
1640   ToggleSuspend( $reserve_id );
1641
1642 This function sets the suspend field to true if is false, and false if it is true.
1643 If the reserve is currently suspended with a suspend_until date, that date will
1644 be cleared when it is unsuspended.
1645
1646 =cut
1647
1648 sub ToggleSuspend {
1649     my ( $reserve_id, $suspend_until ) = @_;
1650
1651     $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1652
1653     my $hold = Koha::Holds->find( $reserve_id );
1654
1655     if ( $hold->is_suspended ) {
1656         $hold->resume()
1657     } else {
1658         $hold->suspend_hold( $suspend_until );
1659     }
1660 }
1661
1662 =head2 SuspendAll
1663
1664   SuspendAll(
1665       borrowernumber   => $borrowernumber,
1666       [ biblionumber   => $biblionumber, ]
1667       [ suspend_until  => $suspend_until, ]
1668       [ suspend        => $suspend ]
1669   );
1670
1671   This function accepts a set of hash keys as its parameters.
1672   It requires either borrowernumber or biblionumber, or both.
1673
1674   suspend_until is wholly optional.
1675
1676 =cut
1677
1678 sub SuspendAll {
1679     my %params = @_;
1680
1681     my $borrowernumber = $params{'borrowernumber'} || undef;
1682     my $biblionumber   = $params{'biblionumber'}   || undef;
1683     my $suspend_until  = $params{'suspend_until'}  || undef;
1684     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1685
1686     $suspend_until = eval { dt_from_string($suspend_until) }
1687       if ( defined($suspend_until) );
1688
1689     return unless ( $borrowernumber || $biblionumber );
1690
1691     my $params;
1692     $params->{found}          = undef;
1693     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1694     $params->{biblionumber}   = $biblionumber if $biblionumber;
1695
1696     my @holds = Koha::Holds->search($params);
1697
1698     if ($suspend) {
1699         map { $_->suspend_hold($suspend_until) } @holds;
1700     }
1701     else {
1702         map { $_->resume() } @holds;
1703     }
1704 }
1705
1706
1707 =head2 _FixPriority
1708
1709   _FixPriority({
1710     reserve_id => $reserve_id,
1711     [rank => $rank,]
1712     [ignoreSetLowestRank => $ignoreSetLowestRank]
1713   });
1714
1715   or
1716
1717   _FixPriority({ biblionumber => $biblionumber});
1718
1719 This routine adjusts the priority of a hold request and holds
1720 on the same bib.
1721
1722 In the first form, where a reserve_id is passed, the priority of the
1723 hold is set to supplied rank, and other holds for that bib are adjusted
1724 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1725 is supplied, all of the holds on that bib have their priority adjusted
1726 as if the second form had been used.
1727
1728 In the second form, where a biblionumber is passed, the holds on that
1729 bib (that are not captured) are sorted in order of increasing priority,
1730 then have reserves.priority set so that the first non-captured hold
1731 has its priority set to 1, the second non-captured hold has its priority
1732 set to 2, and so forth.
1733
1734 In both cases, holds that have the lowestPriority flag on are have their
1735 priority adjusted to ensure that they remain at the end of the line.
1736
1737 Note that the ignoreSetLowestRank parameter is meant to be used only
1738 when _FixPriority calls itself.
1739
1740 =cut
1741
1742 sub _FixPriority {
1743     my ( $params ) = @_;
1744     my $reserve_id = $params->{reserve_id};
1745     my $rank = $params->{rank} // '';
1746     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1747     my $biblionumber = $params->{biblionumber};
1748
1749     my $dbh = C4::Context->dbh;
1750
1751     unless ( $biblionumber ) {
1752         my $res = GetReserve( $reserve_id );
1753         $biblionumber = $res->{biblionumber};
1754     }
1755
1756     if ( $rank eq "del" ) {
1757          CancelReserve({ reserve_id => $reserve_id });
1758     }
1759     elsif ( $rank eq "W" || $rank eq "0" ) {
1760
1761         # make sure priority for waiting or in-transit items is 0
1762         my $query = "
1763             UPDATE reserves
1764             SET    priority = 0
1765             WHERE reserve_id = ?
1766             AND found IN ('W', 'T')
1767         ";
1768         my $sth = $dbh->prepare($query);
1769         $sth->execute( $reserve_id );
1770     }
1771     my @priority;
1772
1773     # get whats left
1774     my $query = "
1775         SELECT reserve_id, borrowernumber, reservedate
1776         FROM   reserves
1777         WHERE  biblionumber   = ?
1778           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1779         ORDER BY priority ASC
1780     ";
1781     my $sth = $dbh->prepare($query);
1782     $sth->execute( $biblionumber );
1783     while ( my $line = $sth->fetchrow_hashref ) {
1784         push( @priority,     $line );
1785     }
1786
1787     # To find the matching index
1788     my $i;
1789     my $key = -1;    # to allow for 0 to be a valid result
1790     for ( $i = 0 ; $i < @priority ; $i++ ) {
1791         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1792             $key = $i;    # save the index
1793             last;
1794         }
1795     }
1796
1797     # if index exists in array then move it to new position
1798     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1799         my $new_rank = $rank -
1800           1;    # $new_rank is what you want the new index to be in the array
1801         my $moving_item = splice( @priority, $key, 1 );
1802         splice( @priority, $new_rank, 0, $moving_item );
1803     }
1804
1805     # now fix the priority on those that are left....
1806     $query = "
1807         UPDATE reserves
1808         SET    priority = ?
1809         WHERE  reserve_id = ?
1810     ";
1811     $sth = $dbh->prepare($query);
1812     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1813         $sth->execute(
1814             $j + 1,
1815             $priority[$j]->{'reserve_id'}
1816         );
1817     }
1818     
1819     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1820     $sth->execute();
1821
1822     unless ( $ignoreSetLowestRank ) {
1823       while ( my $res = $sth->fetchrow_hashref() ) {
1824         _FixPriority({
1825             reserve_id => $res->{'reserve_id'},
1826             rank => '999999',
1827             ignoreSetLowestRank => 1
1828         });
1829       }
1830     }
1831 }
1832
1833 =head2 _Findgroupreserve
1834
1835   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1836
1837 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1838 first match found.  If neither, then we look for non-holds-queue based holds.
1839 Lookahead is the number of days to look in advance.
1840
1841 C<&_Findgroupreserve> returns :
1842 C<@results> is an array of references-to-hash whose keys are mostly
1843 fields from the reserves table of the Koha database, plus
1844 C<biblioitemnumber>.
1845
1846 =cut
1847
1848 sub _Findgroupreserve {
1849     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1850     my $dbh   = C4::Context->dbh;
1851
1852     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1853     # check for exact targeted match
1854     my $item_level_target_query = qq{
1855         SELECT reserves.biblionumber        AS biblionumber,
1856                reserves.borrowernumber      AS borrowernumber,
1857                reserves.reservedate         AS reservedate,
1858                reserves.branchcode          AS branchcode,
1859                reserves.cancellationdate    AS cancellationdate,
1860                reserves.found               AS found,
1861                reserves.reservenotes        AS reservenotes,
1862                reserves.priority            AS priority,
1863                reserves.timestamp           AS timestamp,
1864                biblioitems.biblioitemnumber AS biblioitemnumber,
1865                reserves.itemnumber          AS itemnumber,
1866                reserves.reserve_id          AS reserve_id,
1867                reserves.itemtype            AS itemtype
1868         FROM reserves
1869         JOIN biblioitems USING (biblionumber)
1870         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1871         WHERE found IS NULL
1872         AND priority > 0
1873         AND item_level_request = 1
1874         AND itemnumber = ?
1875         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1876         AND suspend = 0
1877         ORDER BY priority
1878     };
1879     my $sth = $dbh->prepare($item_level_target_query);
1880     $sth->execute($itemnumber, $lookahead||0);
1881     my @results;
1882     if ( my $data = $sth->fetchrow_hashref ) {
1883         push( @results, $data )
1884           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1885     }
1886     return @results if @results;
1887
1888     # check for title-level targeted match
1889     my $title_level_target_query = qq{
1890         SELECT reserves.biblionumber        AS biblionumber,
1891                reserves.borrowernumber      AS borrowernumber,
1892                reserves.reservedate         AS reservedate,
1893                reserves.branchcode          AS branchcode,
1894                reserves.cancellationdate    AS cancellationdate,
1895                reserves.found               AS found,
1896                reserves.reservenotes        AS reservenotes,
1897                reserves.priority            AS priority,
1898                reserves.timestamp           AS timestamp,
1899                biblioitems.biblioitemnumber AS biblioitemnumber,
1900                reserves.itemnumber          AS itemnumber,
1901                reserves.reserve_id          AS reserve_id,
1902                reserves.itemtype            AS itemtype
1903         FROM reserves
1904         JOIN biblioitems USING (biblionumber)
1905         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1906         WHERE found IS NULL
1907         AND priority > 0
1908         AND item_level_request = 0
1909         AND hold_fill_targets.itemnumber = ?
1910         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1911         AND suspend = 0
1912         ORDER BY priority
1913     };
1914     $sth = $dbh->prepare($title_level_target_query);
1915     $sth->execute($itemnumber, $lookahead||0);
1916     @results = ();
1917     if ( my $data = $sth->fetchrow_hashref ) {
1918         push( @results, $data )
1919           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1920     }
1921     return @results if @results;
1922
1923     my $query = qq{
1924         SELECT reserves.biblionumber               AS biblionumber,
1925                reserves.borrowernumber             AS borrowernumber,
1926                reserves.reservedate                AS reservedate,
1927                reserves.waitingdate                AS waitingdate,
1928                reserves.branchcode                 AS branchcode,
1929                reserves.cancellationdate           AS cancellationdate,
1930                reserves.found                      AS found,
1931                reserves.reservenotes               AS reservenotes,
1932                reserves.priority                   AS priority,
1933                reserves.timestamp                  AS timestamp,
1934                reserves.itemnumber                 AS itemnumber,
1935                reserves.reserve_id                 AS reserve_id,
1936                reserves.itemtype                   AS itemtype
1937         FROM reserves
1938         WHERE reserves.biblionumber = ?
1939           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1940           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1941           AND suspend = 0
1942           ORDER BY priority
1943     };
1944     $sth = $dbh->prepare($query);
1945     $sth->execute( $biblio, $itemnumber, $lookahead||0);
1946     @results = ();
1947     while ( my $data = $sth->fetchrow_hashref ) {
1948         push( @results, $data )
1949           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1950     }
1951     return @results;
1952 }
1953
1954 =head2 _koha_notify_reserve
1955
1956   _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1957
1958 Sends a notification to the patron that their hold has been filled (through
1959 ModReserveAffect, _not_ ModReserveFill)
1960
1961 The letter code for this notice may be found using the following query:
1962
1963     select distinct letter_code
1964     from message_transports
1965     inner join message_attributes using (message_attribute_id)
1966     where message_name = 'Hold_Filled'
1967
1968 This will probably sipmly be 'HOLD', but because it is defined in the database,
1969 it is subject to addition or change.
1970
1971 The following tables are availalbe witin the notice:
1972
1973     branches
1974     borrowers
1975     biblio
1976     biblioitems
1977     reserves
1978     items
1979
1980 =cut
1981
1982 sub _koha_notify_reserve {
1983     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1984
1985     my $dbh = C4::Context->dbh;
1986     my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1987
1988     # Try to get the borrower's email address
1989     my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1990
1991     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1992             borrowernumber => $borrowernumber,
1993             message_name => 'Hold_Filled'
1994     } );
1995
1996     my $sth = $dbh->prepare("
1997         SELECT *
1998         FROM   reserves
1999         WHERE  borrowernumber = ?
2000             AND biblionumber = ?
2001     ");
2002     $sth->execute( $borrowernumber, $biblionumber );
2003     my $reserve = $sth->fetchrow_hashref;
2004     my $library = Koha::Libraries->find( $reserve->{branchcode} )->unblessed;
2005
2006     my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
2007
2008     my %letter_params = (
2009         module => 'reserves',
2010         branchcode => $reserve->{branchcode},
2011         tables => {
2012             'branches'       => $library,
2013             'borrowers'      => $borrower,
2014             'biblio'         => $biblionumber,
2015             'biblioitems'    => $biblionumber,
2016             'reserves'       => $reserve,
2017             'items', $reserve->{'itemnumber'},
2018         },
2019         substitute => { today => output_pref( { dt => dt_from_string, dateonly => 1 } ) },
2020     );
2021
2022     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.
2023     my $send_notification = sub {
2024         my ( $mtt, $letter_code ) = (@_);
2025         return unless defined $letter_code;
2026         $letter_params{letter_code} = $letter_code;
2027         $letter_params{message_transport_type} = $mtt;
2028         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
2029         unless ($letter) {
2030             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
2031             return;
2032         }
2033
2034         C4::Letters::EnqueueLetter( {
2035             letter => $letter,
2036             borrowernumber => $borrowernumber,
2037             from_address => $admin_email_address,
2038             message_transport_type => $mtt,
2039         } );
2040     };
2041
2042     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
2043         next if (
2044                ( $mtt eq 'email' and not $to_address ) # No email address
2045             or ( $mtt eq 'sms'   and not $borrower->{smsalertnumber} ) # No SMS number
2046             or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
2047         );
2048
2049         &$send_notification($mtt, $letter_code);
2050         $notification_sent++;
2051     }
2052     #Making sure that a print notification is sent if no other transport types can be utilized.
2053     if (! $notification_sent) {
2054         &$send_notification('print', 'HOLD');
2055     }
2056     
2057 }
2058
2059 =head2 _ShiftPriorityByDateAndPriority
2060
2061   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
2062
2063 This increments the priority of all reserves after the one
2064 with either the lowest date after C<$reservedate>
2065 or the lowest priority after C<$priority>.
2066
2067 It effectively makes room for a new reserve to be inserted with a certain
2068 priority, which is returned.
2069
2070 This is most useful when the reservedate can be set by the user.  It allows
2071 the new reserve to be placed before other reserves that have a later
2072 reservedate.  Since priority also is set by the form in reserves/request.pl
2073 the sub accounts for that too.
2074
2075 =cut
2076
2077 sub _ShiftPriorityByDateAndPriority {
2078     my ( $biblio, $resdate, $new_priority ) = @_;
2079
2080     my $dbh = C4::Context->dbh;
2081     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
2082     my $sth = $dbh->prepare( $query );
2083     $sth->execute( $biblio, $resdate, $new_priority );
2084     my $min_priority = $sth->fetchrow;
2085     # if no such matches are found, $new_priority remains as original value
2086     $new_priority = $min_priority if ( $min_priority );
2087
2088     # Shift the priority up by one; works in conjunction with the next SQL statement
2089     $query = "UPDATE reserves
2090               SET priority = priority+1
2091               WHERE biblionumber = ?
2092               AND borrowernumber = ?
2093               AND reservedate = ?
2094               AND found IS NULL";
2095     my $sth_update = $dbh->prepare( $query );
2096
2097     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
2098     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
2099     $sth = $dbh->prepare( $query );
2100     $sth->execute( $new_priority, $biblio );
2101     while ( my $row = $sth->fetchrow_hashref ) {
2102         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2103     }
2104
2105     return $new_priority;  # so the caller knows what priority they wind up receiving
2106 }
2107
2108 =head2 OPACItemHoldsAllowed
2109
2110   OPACItemHoldsAllowed($item_record,$borrower_record);
2111
2112 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see
2113 if specific item holds are allowed, returns true if so.
2114
2115 =cut
2116
2117 sub OPACItemHoldsAllowed {
2118     my ($item,$borrower) = @_;
2119
2120     my $branchcode = $item->{homebranch} or die "No homebranch";
2121     my $itype;
2122     my $dbh = C4::Context->dbh;
2123     if (C4::Context->preference('item-level_itypes')) {
2124        # We can't trust GetItem to honour the syspref, so safest to do it ourselves
2125        # When GetItem is fixed, we can remove this
2126        $itype = $item->{itype};
2127     }
2128     else {
2129        my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
2130        my $sth = $dbh->prepare($query);
2131        $sth->execute($item->{biblioitemnumber});
2132        if (my $data = $sth->fetchrow_hashref()){
2133            $itype = $data->{itemtype};
2134        }
2135     }
2136
2137     my $query = "SELECT opacitemholds,categorycode,itemtype,branchcode FROM issuingrules WHERE
2138           (issuingrules.categorycode = ? OR issuingrules.categorycode = '*')
2139         AND
2140           (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2141         AND
2142           (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')
2143         ORDER BY
2144           issuingrules.categorycode desc,
2145           issuingrules.itemtype desc,
2146           issuingrules.branchcode desc
2147        LIMIT 1";
2148     my $sth = $dbh->prepare($query);
2149     $sth->execute($borrower->{categorycode},$itype,$branchcode);
2150     my $data = $sth->fetchrow_hashref;
2151     my $opacitemholds = uc substr ($data->{opacitemholds}, 0, 1);
2152     return '' if $opacitemholds eq 'N';
2153     return $opacitemholds;
2154 }
2155
2156 =head2 MoveReserve
2157
2158   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2159
2160 Use when checking out an item to handle reserves
2161 If $cancelreserve boolean is set to true, it will remove existing reserve
2162
2163 =cut
2164
2165 sub MoveReserve {
2166     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2167
2168     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2169     my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
2170     return unless $res;
2171
2172     my $biblionumber     =  $res->{biblionumber};
2173     my $biblioitemnumber = $res->{biblioitemnumber};
2174
2175     if ($res->{borrowernumber} == $borrowernumber) {
2176         ModReserveFill($res);
2177     }
2178     else {
2179         # warn "Reserved";
2180         # The item is reserved by someone else.
2181         # Find this item in the reserves
2182
2183         my $borr_res;
2184         foreach (@$all_reserves) {
2185             $_->{'borrowernumber'} == $borrowernumber or next;
2186             $_->{'biblionumber'}   == $biblionumber   or next;
2187
2188             $borr_res = $_;
2189             last;
2190         }
2191
2192         if ( $borr_res ) {
2193             # The item is reserved by the current patron
2194             ModReserveFill($borr_res);
2195         }
2196
2197         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2198             RevertWaitingStatus({ itemnumber => $itemnumber });
2199         }
2200         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2201             CancelReserve( { reserve_id => $res->{'reserve_id'} } );
2202         }
2203     }
2204 }
2205
2206 =head2 MergeHolds
2207
2208   MergeHolds($dbh,$to_biblio, $from_biblio);
2209
2210 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2211
2212 =cut
2213
2214 sub MergeHolds {
2215     my ( $dbh, $to_biblio, $from_biblio ) = @_;
2216     my $sth = $dbh->prepare(
2217         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2218     );
2219     $sth->execute($from_biblio);
2220     if ( my $data = $sth->fetchrow_hashref() ) {
2221
2222         # holds exist on old record, if not we don't need to do anything
2223         $sth = $dbh->prepare(
2224             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2225         $sth->execute( $to_biblio, $from_biblio );
2226
2227         # Reorder by date
2228         # don't reorder those already waiting
2229
2230         $sth = $dbh->prepare(
2231 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2232         );
2233         my $upd_sth = $dbh->prepare(
2234 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2235         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
2236         );
2237         $sth->execute( $to_biblio, 'W', 'T' );
2238         my $priority = 1;
2239         while ( my $reserve = $sth->fetchrow_hashref() ) {
2240             $upd_sth->execute(
2241                 $priority,                    $to_biblio,
2242                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2243                 $reserve->{'itemnumber'}
2244             );
2245             $priority++;
2246         }
2247     }
2248 }
2249
2250 =head2 RevertWaitingStatus
2251
2252   RevertWaitingStatus({ itemnumber => $itemnumber });
2253
2254   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2255
2256   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2257           item level hold, even if it was only a bibliolevel hold to
2258           begin with. This is because we can no longer know if a hold
2259           was item-level or bib-level after a hold has been set to
2260           waiting status.
2261
2262 =cut
2263
2264 sub RevertWaitingStatus {
2265     my ( $params ) = @_;
2266     my $itemnumber = $params->{'itemnumber'};
2267
2268     return unless ( $itemnumber );
2269
2270     my $dbh = C4::Context->dbh;
2271
2272     ## Get the waiting reserve we want to revert
2273     my $query = "
2274         SELECT * FROM reserves
2275         WHERE itemnumber = ?
2276         AND found IS NOT NULL
2277     ";
2278     my $sth = $dbh->prepare( $query );
2279     $sth->execute( $itemnumber );
2280     my $reserve = $sth->fetchrow_hashref();
2281
2282     ## Increment the priority of all other non-waiting
2283     ## reserves for this bib record
2284     $query = "
2285         UPDATE reserves
2286         SET
2287           priority = priority + 1
2288         WHERE
2289           biblionumber =  ?
2290         AND
2291           priority > 0
2292     ";
2293     $sth = $dbh->prepare( $query );
2294     $sth->execute( $reserve->{'biblionumber'} );
2295
2296     ## Fix up the currently waiting reserve
2297     $query = "
2298     UPDATE reserves
2299     SET
2300       priority = 1,
2301       found = NULL,
2302       waitingdate = NULL
2303     WHERE
2304       reserve_id = ?
2305     ";
2306     $sth = $dbh->prepare( $query );
2307     $sth->execute( $reserve->{'reserve_id'} );
2308     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2309 }
2310
2311 =head2 GetReserveId
2312
2313   $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2314
2315   Returnes the first reserve id that matches the given criteria
2316
2317 =cut
2318
2319 sub GetReserveId {
2320     my ( $params ) = @_;
2321
2322     return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2323
2324     my $dbh = C4::Context->dbh();
2325
2326     my $sql = "SELECT reserve_id FROM reserves WHERE ";
2327
2328     my @params;
2329     my @limits;
2330     foreach my $key ( keys %$params ) {
2331         if ( defined( $params->{$key} ) ) {
2332             push( @limits, "$key = ?" );
2333             push( @params, $params->{$key} );
2334         }
2335     }
2336
2337     $sql .= join( " AND ", @limits );
2338
2339     my $sth = $dbh->prepare( $sql );
2340     $sth->execute( @params );
2341     my $row = $sth->fetchrow_hashref();
2342
2343     return $row->{'reserve_id'};
2344 }
2345
2346 =head2 ReserveSlip
2347
2348   ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2349
2350 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2351
2352 The letter code will be HOLD_SLIP, and the following tables are
2353 available within the slip:
2354
2355     reserves
2356     branches
2357     borrowers
2358     biblio
2359     biblioitems
2360     items
2361
2362 =cut
2363
2364 sub ReserveSlip {
2365     my ($branch, $borrowernumber, $biblionumber) = @_;
2366
2367 #   return unless ( C4::Context->boolean_preference('printreserveslips') );
2368
2369     my $reserve_id = GetReserveId({
2370         biblionumber => $biblionumber,
2371         borrowernumber => $borrowernumber
2372     }) or return;
2373     my $reserve = GetReserveInfo($reserve_id) or return;
2374
2375     return  C4::Letters::GetPreparedLetter (
2376         module => 'circulation',
2377         letter_code => 'HOLD_SLIP',
2378         branchcode => $branch,
2379         tables => {
2380             'reserves'    => $reserve,
2381             'branches'    => $reserve->{branchcode},
2382             'borrowers'   => $reserve->{borrowernumber},
2383             'biblio'      => $reserve->{biblionumber},
2384             'biblioitems' => $reserve->{biblionumber},
2385             'items'       => $reserve->{itemnumber},
2386         },
2387     );
2388 }
2389
2390 =head2 GetReservesControlBranch
2391
2392   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2393
2394   Return the branchcode to be used to determine which reserves
2395   policy applies to a transaction.
2396
2397   C<$item> is a hashref for an item. Only 'homebranch' is used.
2398
2399   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2400
2401 =cut
2402
2403 sub GetReservesControlBranch {
2404     my ( $item, $borrower ) = @_;
2405
2406     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2407
2408     my $branchcode =
2409         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2410       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2411       :                                              undef;
2412
2413     return $branchcode;
2414 }
2415
2416 =head2 CalculatePriority
2417
2418     my $p = CalculatePriority($biblionumber, $resdate);
2419
2420 Calculate priority for a new reserve on biblionumber, placing it at
2421 the end of the line of all holds whose start date falls before
2422 the current system time and that are neither on the hold shelf
2423 or in transit.
2424
2425 The reserve date parameter is optional; if it is supplied, the
2426 priority is based on the set of holds whose start date falls before
2427 the parameter value.
2428
2429 After calculation of this priority, it is recommended to call
2430 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2431 AddReserves.
2432
2433 =cut
2434
2435 sub CalculatePriority {
2436     my ( $biblionumber, $resdate ) = @_;
2437
2438     my $sql = q{
2439         SELECT COUNT(*) FROM reserves
2440         WHERE biblionumber = ?
2441         AND   priority > 0
2442         AND   (found IS NULL OR found = '')
2443     };
2444     #skip found==W or found==T (waiting or transit holds)
2445     if( $resdate ) {
2446         $sql.= ' AND ( reservedate <= ? )';
2447     }
2448     else {
2449         $sql.= ' AND ( reservedate < NOW() )';
2450     }
2451     my $dbh = C4::Context->dbh();
2452     my @row = $dbh->selectrow_array(
2453         $sql,
2454         undef,
2455         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2456     );
2457
2458     return @row ? $row[0]+1 : 1;
2459 }
2460
2461 =head2 IsItemOnHoldAndFound
2462
2463     my $bool = IsItemFoundHold( $itemnumber );
2464
2465     Returns true if the item is currently on hold
2466     and that hold has a non-null found status ( W, T, etc. )
2467
2468 =cut
2469
2470 sub IsItemOnHoldAndFound {
2471     my ($itemnumber) = @_;
2472
2473     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2474
2475     my $found = $rs->count(
2476         {
2477             itemnumber => $itemnumber,
2478             found      => { '!=' => undef }
2479         }
2480     );
2481
2482     return $found;
2483 }
2484
2485 =head1 AUTHOR
2486
2487 Koha Development Team <http://koha-community.org/>
2488
2489 =cut
2490
2491 1;