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