Bug 10543 - Unify item mandatory subfields check
[koha.git] / C4 / Reserves.pm
blob1f89127eec38da87968c57ac6bce86f2aa09426f
1 package C4::Reserves;
3 # Copyright 2000-2002 Katipo Communications
4 # 2006 SAN Ouest Provence
5 # 2007-2010 BibLibre Paul POULAIN
6 # 2011 Catalyst IT
8 # This file is part of Koha.
10 # Koha is free software; you can redistribute it and/or modify it under the
11 # terms of the GNU General Public License as published by the Free Software
12 # Foundation; either version 2 of the License, or (at your option) any later
13 # version.
15 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
16 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
17 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License along
20 # with Koha; if not, write to the Free Software Foundation, Inc.,
21 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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;
33 # for _koha_notify_reserve
34 use C4::Members::Messaging;
35 use C4::Members qw();
36 use C4::Letters;
37 use C4::Branch qw( GetBranchDetail );
38 use C4::Dates qw( format_date_in_iso );
40 use Koha::DateUtils;
42 use List::MoreUtils qw( firstidx );
44 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
46 =head1 NAME
48 C4::Reserves - Koha functions for dealing with reservation.
50 =head1 SYNOPSIS
52 use C4::Reserves;
54 =head1 DESCRIPTION
56 This modules provides somes functions to deal with reservations.
58 Reserves are stored in reserves table.
59 The following columns contains important values :
60 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
61 =0 : then the reserve is being dealed
62 - found : NULL : means the patron requested the 1st available, and we haven't choosen the item
63 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
64 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
65 F(inished) : the reserve has been completed, and is done
66 - itemnumber : empty : the reserve is still unaffected to an item
67 filled: the reserve is attached to an item
68 The complete workflow is :
69 ==== 1st use case ====
70 patron request a document, 1st available : P >0, F=NULL, I=NULL
71 a library having it run "transfertodo", and clic on the list
72 if there is no transfer to do, the reserve waiting
73 patron can pick it up P =0, F=W, I=filled
74 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
75 The pickup library recieve the book, it check in P =0, F=W, I=filled
76 The patron borrow the book P =0, F=F, I=filled
78 ==== 2nd use case ====
79 patron requests a document, a given item,
80 If pickup is holding branch P =0, F=W, I=filled
81 If transfer needed, write in branchtransfer P =0, F=T, I=filled
82 The pickup library receive the book, it checks it in P =0, F=W, I=filled
83 The patron borrow the book P =0, F=F, I=filled
85 =head1 FUNCTIONS
87 =cut
89 BEGIN {
90 # set the version for version checking
91 $VERSION = 3.07.00.049;
92 require Exporter;
93 @ISA = qw(Exporter);
94 @EXPORT = qw(
95 &AddReserve
97 &GetReserve
98 &GetReservesFromItemnumber
99 &GetReservesFromBiblionumber
100 &GetReservesFromBorrowernumber
101 &GetReservesForBranch
102 &GetReservesToBranch
103 &GetReserveCount
104 &GetReserveFee
105 &GetReserveInfo
106 &GetReserveStatus
108 &GetOtherReserves
110 &ModReserveFill
111 &ModReserveAffect
112 &ModReserve
113 &ModReserveStatus
114 &ModReserveCancelAll
115 &ModReserveMinusPriority
116 &MoveReserve
118 &CheckReserves
119 &CanBookBeReserved
120 &CanItemBeReserved
121 &CancelReserve
122 &CancelExpiredReserves
124 &AutoUnsuspendReserves
126 &IsAvailableForItemLevelRequest
128 &AlterPriority
129 &ToggleLowestPriority
131 &ReserveSlip
132 &ToggleSuspend
133 &SuspendAll
135 @EXPORT_OK = qw( MergeHolds );
138 =head2 AddReserve
140 AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
142 =cut
144 sub AddReserve {
145 my (
146 $branch, $borrowernumber, $biblionumber,
147 $constraint, $bibitems, $priority, $resdate, $expdate, $notes,
148 $title, $checkitem, $found
149 ) = @_;
150 my $fee =
151 GetReserveFee($borrowernumber, $biblionumber, $constraint,
152 $bibitems );
153 my $dbh = C4::Context->dbh;
154 my $const = lc substr( $constraint, 0, 1 );
155 $resdate = format_date_in_iso( $resdate ) if ( $resdate );
156 $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
157 if ($expdate) {
158 $expdate = format_date_in_iso( $expdate );
159 } else {
160 undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
162 if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
163 # Make room in reserves for this before those of a later reserve date
164 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
166 my $waitingdate;
168 # If the reserv had the waiting status, we had the value of the resdate
169 if ( $found eq 'W' ) {
170 $waitingdate = $resdate;
173 #eval {
174 # updates take place here
175 if ( $fee > 0 ) {
176 my $nextacctno = &getnextacctno( $borrowernumber );
177 my $query = qq/
178 INSERT INTO accountlines
179 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
180 VALUES
181 (?,?,now(),?,?,'Res',?)
183 my $usth = $dbh->prepare($query);
184 $usth->execute( $borrowernumber, $nextacctno, $fee,
185 "Reserve Charge - $title", $fee );
188 #if ($const eq 'a'){
189 my $query = qq/
190 INSERT INTO reserves
191 (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
192 priority,reservenotes,itemnumber,found,waitingdate,expirationdate)
193 VALUES
194 (?,?,?,?,?,
195 ?,?,?,?,?,?)
197 my $sth = $dbh->prepare($query);
198 $sth->execute(
199 $borrowernumber, $biblionumber, $resdate, $branch,
200 $const, $priority, $notes, $checkitem,
201 $found, $waitingdate, $expdate
204 # Send e-mail to librarian if syspref is active
205 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
206 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
207 my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode});
208 if ( my $letter = C4::Letters::GetPreparedLetter (
209 module => 'reserves',
210 letter_code => 'HOLDPLACED',
211 branchcode => $branch,
212 tables => {
213 'branches' => $branch_details,
214 'borrowers' => $borrower,
215 'biblio' => $biblionumber,
217 ) ) {
219 my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
221 C4::Letters::EnqueueLetter(
222 { letter => $letter,
223 borrowernumber => $borrowernumber,
224 message_transport_type => 'email',
225 from_address => $admin_email_address,
226 to_address => $admin_email_address,
233 ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value?
234 $query = qq/
235 INSERT INTO reserveconstraints
236 (borrowernumber,biblionumber,reservedate,biblioitemnumber)
237 VALUES
238 (?,?,?,?)
240 $sth = $dbh->prepare($query); # keep prepare outside the loop!
241 foreach (@$bibitems) {
242 $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
245 return; # FIXME: why not have a useful return value?
248 =head2 GetReserve
250 $res = GetReserve( $reserve_id );
252 =cut
254 sub GetReserve {
255 my ($reserve_id) = @_;
257 my $dbh = C4::Context->dbh;
258 my $query = "SELECT * FROM reserves WHERE reserve_id = ?";
259 my $sth = $dbh->prepare( $query );
260 $sth->execute( $reserve_id );
261 my $res = $sth->fetchrow_hashref();
262 return $res;
265 =head2 GetReservesFromBiblionumber
267 ($count, $title_reserves) = GetReservesFromBiblionumber($biblionumber);
269 This function gets the list of reservations for one C<$biblionumber>, returning a count
270 of the reserves and an arrayref pointing to the reserves for C<$biblionumber>.
272 =cut
274 sub GetReservesFromBiblionumber {
275 my ($biblionumber) = shift or return (0, []);
276 my ($all_dates) = shift;
277 my $dbh = C4::Context->dbh;
279 # Find the desired items in the reserves
280 my $query = "
281 SELECT reserve_id,
282 branchcode,
283 timestamp AS rtimestamp,
284 priority,
285 biblionumber,
286 borrowernumber,
287 reservedate,
288 constrainttype,
289 found,
290 itemnumber,
291 reservenotes,
292 expirationdate,
293 lowestPriority,
294 suspend,
295 suspend_until
296 FROM reserves
297 WHERE biblionumber = ? ";
298 unless ( $all_dates ) {
299 $query .= "AND reservedate <= CURRENT_DATE()";
301 $query .= "ORDER BY priority";
302 my $sth = $dbh->prepare($query);
303 $sth->execute($biblionumber);
304 my @results;
305 my $i = 0;
306 while ( my $data = $sth->fetchrow_hashref ) {
308 # FIXME - What is this doing? How do constraints work?
309 if ($data->{constrainttype} eq 'o') {
310 $query = '
311 SELECT biblioitemnumber
312 FROM reserveconstraints
313 WHERE biblionumber = ?
314 AND borrowernumber = ?
315 AND reservedate = ?
317 my $csth = $dbh->prepare($query);
318 $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
319 my @bibitemno;
320 while ( my $bibitemnos = $csth->fetchrow_array ) {
321 push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref
323 my $count = scalar @bibitemno;
325 # if we have two or more different specific itemtypes
326 # reserved by same person on same day
327 my $bdata;
328 if ( $count > 1 ) {
329 $bdata = GetBiblioItemData( $bibitemno[$i] ); # FIXME: This doesn't make sense.
330 $i++; # $i can increase each pass, but the next @bibitemno might be smaller?
332 else {
333 # Look up the book we just found.
334 $bdata = GetBiblioItemData( $bibitemno[0] );
336 # Add the results of this latest search to the current
337 # results.
338 # FIXME - An 'each' would probably be more efficient.
339 foreach my $key ( keys %$bdata ) {
340 $data->{$key} = $bdata->{$key};
343 push @results, $data;
345 return ( $#results + 1, \@results );
348 =head2 GetReservesFromItemnumber
350 ( $reservedate, $borrowernumber, $branchcode, $reserve_id ) = GetReservesFromItemnumber($itemnumber);
352 TODO :: Description here
354 =cut
356 sub GetReservesFromItemnumber {
357 my ( $itemnumber, $all_dates ) = @_;
358 my $dbh = C4::Context->dbh;
359 my $query = "
360 SELECT reservedate,borrowernumber,branchcode,reserve_id
361 FROM reserves
362 WHERE itemnumber=?
364 unless ( $all_dates ) {
365 $query .= " AND reservedate <= CURRENT_DATE()";
367 my $sth_res = $dbh->prepare($query);
368 $sth_res->execute($itemnumber);
369 my ( $reservedate, $borrowernumber, $branchcode, $reserve_id ) = $sth_res->fetchrow_array;
370 return ( $reservedate, $borrowernumber, $branchcode, $reserve_id );
373 =head2 GetReservesFromBorrowernumber
375 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
377 TODO :: Descritpion
379 =cut
381 sub GetReservesFromBorrowernumber {
382 my ( $borrowernumber, $status ) = @_;
383 my $dbh = C4::Context->dbh;
384 my $sth;
385 if ($status) {
386 $sth = $dbh->prepare("
387 SELECT *
388 FROM reserves
389 WHERE borrowernumber=?
390 AND found =?
391 ORDER BY reservedate
393 $sth->execute($borrowernumber,$status);
394 } else {
395 $sth = $dbh->prepare("
396 SELECT *
397 FROM reserves
398 WHERE borrowernumber=?
399 ORDER BY reservedate
401 $sth->execute($borrowernumber);
403 my $data = $sth->fetchall_arrayref({});
404 return @$data;
406 #-------------------------------------------------------------------------------------
407 =head2 CanBookBeReserved
409 $error = &CanBookBeReserved($borrowernumber, $biblionumber)
411 =cut
413 sub CanBookBeReserved{
414 my ($borrowernumber, $biblionumber) = @_;
416 my $items = GetItemnumbersForBiblio($biblionumber);
417 #get items linked via host records
418 my @hostitems = get_hostitemnumbers_of($biblionumber);
419 if (@hostitems){
420 push (@$items,@hostitems);
423 foreach my $item (@$items){
424 return 1 if CanItemBeReserved($borrowernumber, $item);
426 return 0;
429 =head2 CanItemBeReserved
431 $error = &CanItemBeReserved($borrowernumber, $itemnumber)
433 This function return 1 if an item can be issued by this borrower.
435 =cut
437 sub CanItemBeReserved{
438 my ($borrowernumber, $itemnumber) = @_;
440 my $dbh = C4::Context->dbh;
441 my $allowedreserves = 0;
443 my $controlbranch = C4::Context->preference('ReservesControlBranch');
444 my $itype = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype";
446 # we retrieve borrowers and items informations #
447 my $item = GetItem($itemnumber);
448 my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
450 # we retrieve user rights on this itemtype and branchcode
451 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
452 FROM issuingrules
453 WHERE (categorycode in (?,'*') )
454 AND (itemtype IN (?,'*'))
455 AND (branchcode IN (?,'*'))
456 ORDER BY
457 categorycode DESC,
458 itemtype DESC,
459 branchcode DESC;"
462 my $querycount ="SELECT
463 count(*) as count
464 FROM reserves
465 LEFT JOIN items USING (itemnumber)
466 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
467 LEFT JOIN borrowers USING (borrowernumber)
468 WHERE borrowernumber = ?
472 my $itemtype = $item->{$itype};
473 my $categorycode = $borrower->{categorycode};
474 my $branchcode = "";
475 my $branchfield = "reserves.branchcode";
477 if( $controlbranch eq "ItemHomeLibrary" ){
478 $branchfield = "items.homebranch";
479 $branchcode = $item->{homebranch};
480 }elsif( $controlbranch eq "PatronLibrary" ){
481 $branchfield = "borrowers.branchcode";
482 $branchcode = $borrower->{branchcode};
485 # we retrieve rights
486 $sth->execute($categorycode, $itemtype, $branchcode);
487 if(my $rights = $sth->fetchrow_hashref()){
488 $itemtype = $rights->{itemtype};
489 $allowedreserves = $rights->{reservesallowed};
490 }else{
491 $itemtype = '*';
494 # we retrieve count
496 $querycount .= "AND $branchfield = ?";
498 $querycount .= " AND $itype = ?" if ($itemtype ne "*");
499 my $sthcount = $dbh->prepare($querycount);
501 if($itemtype eq "*"){
502 $sthcount->execute($borrowernumber, $branchcode);
503 }else{
504 $sthcount->execute($borrowernumber, $branchcode, $itemtype);
507 my $reservecount = "0";
508 if(my $rowcount = $sthcount->fetchrow_hashref()){
509 $reservecount = $rowcount->{count};
512 # we check if it's ok or not
513 if( $reservecount < $allowedreserves ){
514 return 1;
515 }else{
516 return 0;
519 #--------------------------------------------------------------------------------
520 =head2 GetReserveCount
522 $number = &GetReserveCount($borrowernumber);
524 this function returns the number of reservation for a borrower given on input arg.
526 =cut
528 sub GetReserveCount {
529 my ($borrowernumber) = @_;
531 my $dbh = C4::Context->dbh;
533 my $query = "
534 SELECT COUNT(*) AS counter
535 FROM reserves
536 WHERE borrowernumber = ?
538 my $sth = $dbh->prepare($query);
539 $sth->execute($borrowernumber);
540 my $row = $sth->fetchrow_hashref;
541 return $row->{counter};
544 =head2 GetOtherReserves
546 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
548 Check queued list of this document and check if this document must be transfered
550 =cut
552 sub GetOtherReserves {
553 my ($itemnumber) = @_;
554 my $messages;
555 my $nextreservinfo;
556 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
557 if ($checkreserves) {
558 my $iteminfo = GetItem($itemnumber);
559 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
560 $messages->{'transfert'} = $checkreserves->{'branchcode'};
561 #minus priorities of others reservs
562 ModReserveMinusPriority(
563 $itemnumber,
564 $checkreserves->{'reserve_id'},
567 #launch the subroutine dotransfer
568 C4::Items::ModItemTransfer(
569 $itemnumber,
570 $iteminfo->{'holdingbranch'},
571 $checkreserves->{'branchcode'}
576 #step 2b : case of a reservation on the same branch, set the waiting status
577 else {
578 $messages->{'waiting'} = 1;
579 ModReserveMinusPriority(
580 $itemnumber,
581 $checkreserves->{'reserve_id'},
583 ModReserveStatus($itemnumber,'W');
586 $nextreservinfo = $checkreserves->{'borrowernumber'};
589 return ( $messages, $nextreservinfo );
592 =head2 GetReserveFee
594 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
596 Calculate the fee for a reserve
598 =cut
600 sub GetReserveFee {
601 my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
603 #check for issues;
604 my $dbh = C4::Context->dbh;
605 my $const = lc substr( $constraint, 0, 1 );
606 my $query = qq/
607 SELECT * FROM borrowers
608 LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
609 WHERE borrowernumber = ?
611 my $sth = $dbh->prepare($query);
612 $sth->execute($borrowernumber);
613 my $data = $sth->fetchrow_hashref;
614 $sth->finish();
615 my $fee = $data->{'reservefee'};
616 my $cntitems = @- > $bibitems;
618 if ( $fee > 0 ) {
620 # check for items on issue
621 # first find biblioitem records
622 my @biblioitems;
623 my $sth1 = $dbh->prepare(
624 "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
625 WHERE (biblio.biblionumber = ?)"
627 $sth1->execute($biblionumber);
628 while ( my $data1 = $sth1->fetchrow_hashref ) {
629 if ( $const eq "a" ) {
630 push @biblioitems, $data1;
632 else {
633 my $found = 0;
634 my $x = 0;
635 while ( $x < $cntitems ) {
636 if ( @$bibitems->{'biblioitemnumber'} ==
637 $data->{'biblioitemnumber'} )
639 $found = 1;
641 $x++;
643 if ( $const eq 'o' ) {
644 if ( $found == 1 ) {
645 push @biblioitems, $data1;
648 else {
649 if ( $found == 0 ) {
650 push @biblioitems, $data1;
655 $sth1->finish;
656 my $cntitemsfound = @biblioitems;
657 my $issues = 0;
658 my $x = 0;
659 my $allissued = 1;
660 while ( $x < $cntitemsfound ) {
661 my $bitdata = $biblioitems[$x];
662 my $sth2 = $dbh->prepare(
663 "SELECT * FROM items
664 WHERE biblioitemnumber = ?"
666 $sth2->execute( $bitdata->{'biblioitemnumber'} );
667 while ( my $itdata = $sth2->fetchrow_hashref ) {
668 my $sth3 = $dbh->prepare(
669 "SELECT * FROM issues
670 WHERE itemnumber = ?"
672 $sth3->execute( $itdata->{'itemnumber'} );
673 if ( my $isdata = $sth3->fetchrow_hashref ) {
675 else {
676 $allissued = 0;
679 $x++;
681 if ( $allissued == 0 ) {
682 my $rsth =
683 $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
684 $rsth->execute($biblionumber);
685 if ( my $rdata = $rsth->fetchrow_hashref ) {
687 else {
688 $fee = 0;
692 return $fee;
695 =head2 GetReservesToBranch
697 @transreserv = GetReservesToBranch( $frombranch );
699 Get reserve list for a given branch
701 =cut
703 sub GetReservesToBranch {
704 my ( $frombranch ) = @_;
705 my $dbh = C4::Context->dbh;
706 my $sth = $dbh->prepare(
707 "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
708 FROM reserves
709 WHERE priority='0'
710 AND branchcode=?"
712 $sth->execute( $frombranch );
713 my @transreserv;
714 my $i = 0;
715 while ( my $data = $sth->fetchrow_hashref ) {
716 $transreserv[$i] = $data;
717 $i++;
719 return (@transreserv);
722 =head2 GetReservesForBranch
724 @transreserv = GetReservesForBranch($frombranch);
726 =cut
728 sub GetReservesForBranch {
729 my ($frombranch) = @_;
730 my $dbh = C4::Context->dbh;
732 my $query = "
733 SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
734 FROM reserves
735 WHERE priority='0'
736 AND found='W'
738 $query .= " AND branchcode=? " if ( $frombranch );
739 $query .= "ORDER BY waitingdate" ;
741 my $sth = $dbh->prepare($query);
742 if ($frombranch){
743 $sth->execute($frombranch);
744 } else {
745 $sth->execute();
748 my @transreserv;
749 my $i = 0;
750 while ( my $data = $sth->fetchrow_hashref ) {
751 $transreserv[$i] = $data;
752 $i++;
754 return (@transreserv);
757 =head2 GetReserveStatus
759 $reservestatus = GetReserveStatus($itemnumber, $biblionumber);
761 Take an itemnumber or a biblionumber and return the status of the reserve places on it.
762 If several reserves exist, the reserve with the lower priority is given.
764 =cut
766 ## FIXME: I don't think this does what it thinks it does.
767 ## It only ever checks the first reserve result, even though
768 ## multiple reserves for that bib can have the itemnumber set
769 ## the sub is only used once in the codebase.
770 sub GetReserveStatus {
771 my ($itemnumber, $biblionumber) = @_;
773 my $dbh = C4::Context->dbh;
775 my ($sth, $found, $priority);
776 if ( $itemnumber ) {
777 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
778 $sth->execute($itemnumber);
779 ($found, $priority) = $sth->fetchrow_array;
782 if ( $biblionumber and not defined $found and not defined $priority ) {
783 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE biblionumber = ? order by priority LIMIT 1");
784 $sth->execute($biblionumber);
785 ($found, $priority) = $sth->fetchrow_array;
788 if(defined $found) {
789 return 'Waiting' if $found eq 'W' and $priority == 0;
790 return 'Finished' if $found eq 'F';
791 return 'Reserved' if $priority > 0;
793 return '';
794 #empty string here will remove need for checking undef, or less log lines
797 =head2 CheckReserves
799 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
800 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
802 Find a book in the reserves.
804 C<$itemnumber> is the book's item number.
806 As I understand it, C<&CheckReserves> looks for the given item in the
807 reserves. If it is found, that's a match, and C<$status> is set to
808 C<Waiting>.
810 Otherwise, it finds the most important item in the reserves with the
811 same biblio number as this book (I'm not clear on this) and returns it
812 with C<$status> set to C<Reserved>.
814 C<&CheckReserves> returns a two-element list:
816 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
818 C<$reserve> is the reserve item that matched. It is a
819 reference-to-hash whose keys are mostly the fields of the reserves
820 table in the Koha database.
822 =cut
824 sub CheckReserves {
825 my ( $item, $barcode ) = @_;
826 my $dbh = C4::Context->dbh;
827 my $sth;
828 my $select;
829 if (C4::Context->preference('item-level_itypes')){
830 $select = "
831 SELECT items.biblionumber,
832 items.biblioitemnumber,
833 itemtypes.notforloan,
834 items.notforloan AS itemnotforloan,
835 items.itemnumber
836 FROM items
837 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
838 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
841 else {
842 $select = "
843 SELECT items.biblionumber,
844 items.biblioitemnumber,
845 itemtypes.notforloan,
846 items.notforloan AS itemnotforloan,
847 items.itemnumber
848 FROM items
849 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
850 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
854 if ($item) {
855 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
856 $sth->execute($item);
858 else {
859 $sth = $dbh->prepare("$select WHERE barcode = ?");
860 $sth->execute($barcode);
862 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
863 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
865 return ( '' ) unless $itemnumber; # bail if we got nothing.
867 # if item is not for loan it cannot be reserved either.....
868 # execpt where items.notforloan < 0 : This indicates the item is holdable.
869 return ( '' ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
871 # Find this item in the reserves
872 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber );
874 # $priority and $highest are used to find the most important item
875 # in the list returned by &_Findgroupreserve. (The lower $priority,
876 # the more important the item.)
877 # $highest is the most important item we've seen so far.
878 my $highest;
879 if (scalar @reserves) {
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 # See if this item is more important than what we've got so far
886 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
887 my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'});
888 my $iteminfo=C4::Items::GetItem($itemnumber);
889 my $branch=C4::Circulation::_GetCircControlBranch($iteminfo,$borrowerinfo);
890 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
891 next if ($branchitemrule->{'holdallowed'} == 0);
892 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
893 $priority = $res->{'priority'};
894 $highest = $res;
900 # If we get this far, then no exact match was found.
901 # We return the most important (i.e. next) reservation.
902 if ($highest) {
903 $highest->{'itemnumber'} = $item;
904 return ( "Reserved", $highest, \@reserves );
907 return ( '' );
910 =head2 CancelExpiredReserves
912 CancelExpiredReserves();
914 Cancels all reserves with an expiration date from before today.
916 =cut
918 sub CancelExpiredReserves {
920 # Cancel reserves that have passed their expiration date.
921 my $dbh = C4::Context->dbh;
922 my $sth = $dbh->prepare( "
923 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
924 AND expirationdate IS NOT NULL
925 AND found IS NULL
926 " );
927 $sth->execute();
929 while ( my $res = $sth->fetchrow_hashref() ) {
930 CancelReserve({ reserve_id => $res->{'reserve_id'} });
933 # Cancel reserves that have been waiting too long
934 if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
935 my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
936 my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
938 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
939 $sth = $dbh->prepare( $query );
940 $sth->execute( $max_pickup_delay );
942 while (my $res = $sth->fetchrow_hashref ) {
943 if ( $charge ) {
944 manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
947 CancelReserve({ reserve_id => $res->{'reserve_id'} });
953 =head2 AutoUnsuspendReserves
955 AutoUnsuspendReserves();
957 Unsuspends all suspended reserves with a suspend_until date from before today.
959 =cut
961 sub AutoUnsuspendReserves {
963 my $dbh = C4::Context->dbh;
965 my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
966 my $sth = $dbh->prepare( $query );
967 $sth->execute();
971 =head2 CancelReserve
973 CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber ] });
975 Cancels a reserve.
977 =cut
979 sub CancelReserve {
980 my ( $params ) = @_;
982 my $reserve_id = $params->{'reserve_id'};
983 $reserve_id = GetReserveId( $params ) unless ( $reserve_id );
985 return unless ( $reserve_id );
987 my $dbh = C4::Context->dbh;
989 my $query = "
990 UPDATE reserves
991 SET cancellationdate = now(),
992 found = Null,
993 priority = 0
994 WHERE reserve_id = ?
996 my $sth = $dbh->prepare($query);
997 $sth->execute( $reserve_id );
998 $sth->finish;
1000 $query = "
1001 INSERT INTO old_reserves
1002 SELECT * FROM reserves
1003 WHERE reserve_id = ?
1005 $sth = $dbh->prepare($query);
1006 $sth->execute( $reserve_id );
1008 $query = "
1009 DELETE FROM reserves
1010 WHERE reserve_id = ?
1012 $sth = $dbh->prepare($query);
1013 $sth->execute( $reserve_id );
1015 # now fix the priority on the others....
1016 _FixPriority( $reserve_id );
1019 =head2 ModReserve
1021 ModReserve({ rank => $rank,
1022 reserve_id => $reserve_id,
1023 branchcode => $branchcode
1024 [, itemnumber => $itemnumber ]
1025 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1028 Change a hold request's priority or cancel it.
1030 C<$rank> specifies the effect of the change. If C<$rank>
1031 is 'W' or 'n', nothing happens. This corresponds to leaving a
1032 request alone when changing its priority in the holds queue
1033 for a bib.
1035 If C<$rank> is 'del', the hold request is cancelled.
1037 If C<$rank> is an integer greater than zero, the priority of
1038 the request is set to that value. Since priority != 0 means
1039 that the item is not waiting on the hold shelf, setting the
1040 priority to a non-zero value also sets the request's found
1041 status and waiting date to NULL.
1043 The optional C<$itemnumber> parameter is used only when
1044 C<$rank> is a non-zero integer; if supplied, the itemnumber
1045 of the hold request is set accordingly; if omitted, the itemnumber
1046 is cleared.
1048 B<FIXME:> Note that the forgoing can have the effect of causing
1049 item-level hold requests to turn into title-level requests. This
1050 will be fixed once reserves has separate columns for requested
1051 itemnumber and supplying itemnumber.
1053 =cut
1055 sub ModReserve {
1056 my ( $params ) = @_;
1058 my $rank = $params->{'rank'};
1059 my $reserve_id = $params->{'reserve_id'};
1060 my $branchcode = $params->{'branchcode'};
1061 my $itemnumber = $params->{'itemnumber'};
1062 my $suspend_until = $params->{'suspend_until'};
1063 my $borrowernumber = $params->{'borrowernumber'};
1064 my $biblionumber = $params->{'biblionumber'};
1066 return if $rank eq "W";
1067 return if $rank eq "n";
1069 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1070 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1072 my $dbh = C4::Context->dbh;
1073 if ( $rank eq "del" ) {
1074 my $query = "
1075 UPDATE reserves
1076 SET cancellationdate=now()
1077 WHERE reserve_id = ?
1079 my $sth = $dbh->prepare($query);
1080 $sth->execute( $reserve_id );
1081 $sth->finish;
1082 $query = "
1083 INSERT INTO old_reserves
1084 SELECT *
1085 FROM reserves
1086 WHERE reserve_id = ?
1088 $sth = $dbh->prepare($query);
1089 $sth->execute( $reserve_id );
1090 $query = "
1091 DELETE FROM reserves
1092 WHERE reserve_id = ?
1094 $sth = $dbh->prepare($query);
1095 $sth->execute( $reserve_id );
1098 elsif ($rank =~ /^\d+/ and $rank > 0) {
1099 my $query = "
1100 UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1101 WHERE reserve_id = ?
1103 my $sth = $dbh->prepare($query);
1104 $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
1105 $sth->finish;
1107 if ( defined( $suspend_until ) ) {
1108 if ( $suspend_until ) {
1109 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1110 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
1111 } else {
1112 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
1116 _FixPriority( $reserve_id, $rank );
1120 =head2 ModReserveFill
1122 &ModReserveFill($reserve);
1124 Fill a reserve. If I understand this correctly, this means that the
1125 reserved book has been found and given to the patron who reserved it.
1127 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1128 whose keys are fields from the reserves table in the Koha database.
1130 =cut
1132 sub ModReserveFill {
1133 my ($res) = @_;
1134 my $dbh = C4::Context->dbh;
1135 # fill in a reserve record....
1136 my $reserve_id = $res->{'reserve_id'};
1137 my $biblionumber = $res->{'biblionumber'};
1138 my $borrowernumber = $res->{'borrowernumber'};
1139 my $resdate = $res->{'reservedate'};
1141 # get the priority on this record....
1142 my $priority;
1143 my $query = "SELECT priority
1144 FROM reserves
1145 WHERE biblionumber = ?
1146 AND borrowernumber = ?
1147 AND reservedate = ?";
1148 my $sth = $dbh->prepare($query);
1149 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1150 ($priority) = $sth->fetchrow_array;
1151 $sth->finish;
1153 # update the database...
1154 $query = "UPDATE reserves
1155 SET found = 'F',
1156 priority = 0
1157 WHERE biblionumber = ?
1158 AND reservedate = ?
1159 AND borrowernumber = ?
1161 $sth = $dbh->prepare($query);
1162 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1163 $sth->finish;
1165 # move to old_reserves
1166 $query = "INSERT INTO old_reserves
1167 SELECT * FROM reserves
1168 WHERE biblionumber = ?
1169 AND reservedate = ?
1170 AND borrowernumber = ?
1172 $sth = $dbh->prepare($query);
1173 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1174 $query = "DELETE FROM reserves
1175 WHERE biblionumber = ?
1176 AND reservedate = ?
1177 AND borrowernumber = ?
1179 $sth = $dbh->prepare($query);
1180 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1182 # now fix the priority on the others (if the priority wasn't
1183 # already sorted!)....
1184 unless ( $priority == 0 ) {
1185 _FixPriority( $reserve_id );
1189 =head2 ModReserveStatus
1191 &ModReserveStatus($itemnumber, $newstatus);
1193 Update the reserve status for the active (priority=0) reserve.
1195 $itemnumber is the itemnumber the reserve is on
1197 $newstatus is the new status.
1199 =cut
1201 sub ModReserveStatus {
1203 #first : check if we have a reservation for this item .
1204 my ($itemnumber, $newstatus) = @_;
1205 my $dbh = C4::Context->dbh;
1207 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1208 my $sth_set = $dbh->prepare($query);
1209 $sth_set->execute( $newstatus, $itemnumber );
1211 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1212 CartToShelf( $itemnumber );
1216 =head2 ModReserveAffect
1218 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1220 This function affect an item and a status for a given reserve
1221 The itemnumber parameter is used to find the biblionumber.
1222 with the biblionumber & the borrowernumber, we can affect the itemnumber
1223 to the correct reserve.
1225 if $transferToDo is not set, then the status is set to "Waiting" as well.
1226 otherwise, a transfer is on the way, and the end of the transfer will
1227 take care of the waiting status
1229 =cut
1231 sub ModReserveAffect {
1232 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1233 my $dbh = C4::Context->dbh;
1235 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1236 # attached to $itemnumber
1237 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1238 $sth->execute($itemnumber);
1239 my ($biblionumber) = $sth->fetchrow;
1241 # get request - need to find out if item is already
1242 # waiting in order to not send duplicate hold filled notifications
1243 my $request = GetReserveInfo($borrowernumber, $biblionumber);
1244 my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1246 # If we affect a reserve that has to be transfered, don't set to Waiting
1247 my $query;
1248 if ($transferToDo) {
1249 $query = "
1250 UPDATE reserves
1251 SET priority = 0,
1252 itemnumber = ?,
1253 found = 'T'
1254 WHERE borrowernumber = ?
1255 AND biblionumber = ?
1258 else {
1259 # affect the reserve to Waiting as well.
1260 $query = "
1261 UPDATE reserves
1262 SET priority = 0,
1263 found = 'W',
1264 waitingdate = NOW(),
1265 itemnumber = ?
1266 WHERE borrowernumber = ?
1267 AND biblionumber = ?
1270 $sth = $dbh->prepare($query);
1271 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1272 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1274 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1275 CartToShelf( $itemnumber );
1278 return;
1281 =head2 ModReserveCancelAll
1283 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1285 function to cancel reserv,check other reserves, and transfer document if it's necessary
1287 =cut
1289 sub ModReserveCancelAll {
1290 my $messages;
1291 my $nextreservinfo;
1292 my ( $itemnumber, $borrowernumber ) = @_;
1294 #step 1 : cancel the reservation
1295 my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1297 #step 2 launch the subroutine of the others reserves
1298 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1300 return ( $messages, $nextreservinfo );
1303 =head2 ModReserveMinusPriority
1305 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1307 Reduce the values of queued list
1309 =cut
1311 sub ModReserveMinusPriority {
1312 my ( $itemnumber, $reserve_id ) = @_;
1314 #first step update the value of the first person on reserv
1315 my $dbh = C4::Context->dbh;
1316 my $query = "
1317 UPDATE reserves
1318 SET priority = 0 , itemnumber = ?
1319 WHERE reserve_id = ?
1321 my $sth_upd = $dbh->prepare($query);
1322 $sth_upd->execute( $itemnumber, $reserve_id );
1323 # second step update all others reservs
1324 _FixPriority( $reserve_id, '0');
1327 =head2 GetReserveInfo
1329 &GetReserveInfo($reserve_id);
1331 Get item and borrower details for a current hold.
1332 Current implementation this query should have a single result.
1334 =cut
1336 sub GetReserveInfo {
1337 my ( $reserve_id ) = @_;
1338 my $dbh = C4::Context->dbh;
1339 my $strsth="SELECT
1340 reserve_id,
1341 reservedate,
1342 reservenotes,
1343 reserves.borrowernumber,
1344 reserves.biblionumber,
1345 reserves.branchcode,
1346 reserves.waitingdate,
1347 notificationdate,
1348 reminderdate,
1349 priority,
1350 found,
1351 firstname,
1352 surname,
1353 phone,
1354 email,
1355 address,
1356 address2,
1357 cardnumber,
1358 city,
1359 zipcode,
1360 biblio.title,
1361 biblio.author,
1362 items.holdingbranch,
1363 items.itemcallnumber,
1364 items.itemnumber,
1365 items.location,
1366 barcode,
1367 notes
1368 FROM reserves
1369 LEFT JOIN items USING(itemnumber)
1370 LEFT JOIN borrowers USING(borrowernumber)
1371 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1372 WHERE reserves.reserve_id = ?";
1373 my $sth = $dbh->prepare($strsth);
1374 $sth->execute($reserve_id);
1376 my $data = $sth->fetchrow_hashref;
1377 return $data;
1380 =head2 IsAvailableForItemLevelRequest
1382 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1384 Checks whether a given item record is available for an
1385 item-level hold request. An item is available if
1387 * it is not lost AND
1388 * it is not damaged AND
1389 * it is not withdrawn AND
1390 * does not have a not for loan value > 0
1392 Whether or not the item is currently on loan is
1393 also checked - if the AllowOnShelfHolds system preference
1394 is ON, an item can be requested even if it is currently
1395 on loan to somebody else. If the system preference
1396 is OFF, an item that is currently checked out cannot
1397 be the target of an item-level hold request.
1399 Note that IsAvailableForItemLevelRequest() does not
1400 check if the staff operator is authorized to place
1401 a request on the item - in particular,
1402 this routine does not check IndependentBranches
1403 and canreservefromotherbranches.
1405 =cut
1407 sub IsAvailableForItemLevelRequest {
1408 my $itemnumber = shift;
1410 my $item = GetItem($itemnumber);
1412 # must check the notforloan setting of the itemtype
1413 # FIXME - a lot of places in the code do this
1414 # or something similar - need to be
1415 # consolidated
1416 my $dbh = C4::Context->dbh;
1417 my $notforloan_query;
1418 if (C4::Context->preference('item-level_itypes')) {
1419 $notforloan_query = "SELECT itemtypes.notforloan
1420 FROM items
1421 JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1422 WHERE itemnumber = ?";
1423 } else {
1424 $notforloan_query = "SELECT itemtypes.notforloan
1425 FROM items
1426 JOIN biblioitems USING (biblioitemnumber)
1427 JOIN itemtypes USING (itemtype)
1428 WHERE itemnumber = ?";
1430 my $sth = $dbh->prepare($notforloan_query);
1431 $sth->execute($itemnumber);
1432 my $notforloan_per_itemtype = 0;
1433 if (my ($notforloan) = $sth->fetchrow_array) {
1434 $notforloan_per_itemtype = 1 if $notforloan;
1437 my $available_per_item = 1;
1438 $available_per_item = 0 if $item->{itemlost} or
1439 ( $item->{notforloan} > 0 ) or
1440 ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1441 $item->{wthdrawn} or
1442 $notforloan_per_itemtype;
1445 if (C4::Context->preference('AllowOnShelfHolds')) {
1446 return $available_per_item;
1447 } else {
1448 return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "Waiting"));
1452 =head2 AlterPriority
1454 AlterPriority( $where, $reserve_id );
1456 This function changes a reserve's priority up, down, to the top, or to the bottom.
1457 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1459 =cut
1461 sub AlterPriority {
1462 my ( $where, $reserve_id ) = @_;
1464 my $dbh = C4::Context->dbh;
1466 my $reserve = GetReserve( $reserve_id );
1468 if ( $reserve->{cancellationdate} ) {
1469 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1470 return;
1473 if ( $where eq 'up' || $where eq 'down' ) {
1475 my $priority = $reserve->{'priority'};
1476 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1477 _FixPriority( $reserve_id, $priority )
1479 } elsif ( $where eq 'top' ) {
1481 _FixPriority( $reserve_id, '1' )
1483 } elsif ( $where eq 'bottom' ) {
1485 _FixPriority( $reserve_id, '999999' )
1490 =head2 ToggleLowestPriority
1492 ToggleLowestPriority( $borrowernumber, $biblionumber );
1494 This function sets the lowestPriority field to true if is false, and false if it is true.
1496 =cut
1498 sub ToggleLowestPriority {
1499 my ( $reserve_id ) = @_;
1501 my $dbh = C4::Context->dbh;
1503 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1504 $sth->execute( $reserve_id );
1505 $sth->finish;
1507 _FixPriority( $reserve_id, '999999' );
1510 =head2 ToggleSuspend
1512 ToggleSuspend( $reserve_id );
1514 This function sets the suspend field to true if is false, and false if it is true.
1515 If the reserve is currently suspended with a suspend_until date, that date will
1516 be cleared when it is unsuspended.
1518 =cut
1520 sub ToggleSuspend {
1521 my ( $reserve_id, $suspend_until ) = @_;
1523 $suspend_until = output_pref( dt_from_string( $suspend_until ), 'iso' ) if ( $suspend_until );
1525 my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1527 my $dbh = C4::Context->dbh;
1529 my $sth = $dbh->prepare(
1530 "UPDATE reserves SET suspend = NOT suspend,
1531 suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1532 WHERE reserve_id = ?
1535 my @params;
1536 push( @params, $suspend_until ) if ( $suspend_until );
1537 push( @params, $reserve_id );
1539 $sth->execute( @params );
1540 $sth->finish;
1543 =head2 SuspendAll
1545 SuspendAll(
1546 borrowernumber => $borrowernumber,
1547 [ biblionumber => $biblionumber, ]
1548 [ suspend_until => $suspend_until, ]
1549 [ suspend => $suspend ]
1552 This function accepts a set of hash keys as its parameters.
1553 It requires either borrowernumber or biblionumber, or both.
1555 suspend_until is wholly optional.
1557 =cut
1559 sub SuspendAll {
1560 my %params = @_;
1562 my $borrowernumber = $params{'borrowernumber'} || undef;
1563 my $biblionumber = $params{'biblionumber'} || undef;
1564 my $suspend_until = $params{'suspend_until'} || undef;
1565 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1567 $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1569 return unless ( $borrowernumber || $biblionumber );
1571 my ( $query, $sth, $dbh, @query_params );
1573 $query = "UPDATE reserves SET suspend = ? ";
1574 push( @query_params, $suspend );
1575 if ( !$suspend ) {
1576 $query .= ", suspend_until = NULL ";
1577 } elsif ( $suspend_until ) {
1578 $query .= ", suspend_until = ? ";
1579 push( @query_params, $suspend_until );
1581 $query .= " WHERE ";
1582 if ( $borrowernumber ) {
1583 $query .= " borrowernumber = ? ";
1584 push( @query_params, $borrowernumber );
1586 $query .= " AND " if ( $borrowernumber && $biblionumber );
1587 if ( $biblionumber ) {
1588 $query .= " biblionumber = ? ";
1589 push( @query_params, $biblionumber );
1591 $query .= " AND found IS NULL ";
1593 $dbh = C4::Context->dbh;
1594 $sth = $dbh->prepare( $query );
1595 $sth->execute( @query_params );
1596 $sth->finish;
1600 =head2 _FixPriority
1602 &_FixPriority( $reserve_id, $rank, $ignoreSetLowestRank);
1604 Only used internally (so don't export it)
1605 Changed how this functions works #
1606 Now just gets an array of reserves in the rank order and updates them with
1607 the array index (+1 as array starts from 0)
1608 and if $rank is supplied will splice item from the array and splice it back in again
1609 in new priority rank
1611 =cut
1613 sub _FixPriority {
1614 my ( $reserve_id, $rank, $ignoreSetLowestRank ) = @_;
1615 my $dbh = C4::Context->dbh;
1617 my $res = GetReserve( $reserve_id );
1619 if ( $rank eq "del" ) {
1620 CancelReserve({ reserve_id => $reserve_id });
1622 elsif ( $rank eq "W" || $rank eq "0" ) {
1624 # make sure priority for waiting or in-transit items is 0
1625 my $query = "
1626 UPDATE reserves
1627 SET priority = 0
1628 WHERE reserve_id = ?
1629 AND found IN ('W', 'T')
1631 my $sth = $dbh->prepare($query);
1632 $sth->execute( $reserve_id );
1634 my @priority;
1636 # get whats left
1637 my $query = "
1638 SELECT reserve_id, borrowernumber, reservedate, constrainttype
1639 FROM reserves
1640 WHERE biblionumber = ?
1641 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1642 ORDER BY priority ASC
1644 my $sth = $dbh->prepare($query);
1645 $sth->execute( $res->{'biblionumber'} );
1646 while ( my $line = $sth->fetchrow_hashref ) {
1647 push( @priority, $line );
1650 # To find the matching index
1651 my $i;
1652 my $key = -1; # to allow for 0 to be a valid result
1653 for ( $i = 0 ; $i < @priority ; $i++ ) {
1654 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1655 $key = $i; # save the index
1656 last;
1660 # if index exists in array then move it to new position
1661 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1662 my $new_rank = $rank -
1663 1; # $new_rank is what you want the new index to be in the array
1664 my $moving_item = splice( @priority, $key, 1 );
1665 splice( @priority, $new_rank, 0, $moving_item );
1668 # now fix the priority on those that are left....
1669 $query = "
1670 UPDATE reserves
1671 SET priority = ?
1672 WHERE reserve_id = ?
1674 $sth = $dbh->prepare($query);
1675 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1676 $sth->execute(
1677 $j + 1,
1678 $priority[$j]->{'reserve_id'}
1680 $sth->finish;
1683 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1684 $sth->execute();
1686 unless ( $ignoreSetLowestRank ) {
1687 while ( my $res = $sth->fetchrow_hashref() ) {
1688 _FixPriority( $res->{'reserve_id'}, '999999', 1 );
1693 =head2 _Findgroupreserve
1695 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1697 Looks for an item-specific match first, then for a title-level match, returning the
1698 first match found. If neither, then we look for a 3rd kind of match based on
1699 reserve constraints.
1701 TODO: add more explanation about reserve constraints
1703 C<&_Findgroupreserve> returns :
1704 C<@results> is an array of references-to-hash whose keys are mostly
1705 fields from the reserves table of the Koha database, plus
1706 C<biblioitemnumber>.
1708 =cut
1710 sub _Findgroupreserve {
1711 my ( $bibitem, $biblio, $itemnumber ) = @_;
1712 my $dbh = C4::Context->dbh;
1714 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1715 # check for exact targetted match
1716 my $item_level_target_query = qq/
1717 SELECT reserves.biblionumber AS biblionumber,
1718 reserves.borrowernumber AS borrowernumber,
1719 reserves.reservedate AS reservedate,
1720 reserves.branchcode AS branchcode,
1721 reserves.cancellationdate AS cancellationdate,
1722 reserves.found AS found,
1723 reserves.reservenotes AS reservenotes,
1724 reserves.priority AS priority,
1725 reserves.timestamp AS timestamp,
1726 biblioitems.biblioitemnumber AS biblioitemnumber,
1727 reserves.itemnumber AS itemnumber,
1728 reserves.reserve_id AS reserve_id
1729 FROM reserves
1730 JOIN biblioitems USING (biblionumber)
1731 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1732 WHERE found IS NULL
1733 AND priority > 0
1734 AND item_level_request = 1
1735 AND itemnumber = ?
1736 AND reservedate <= CURRENT_DATE()
1737 AND suspend = 0
1739 my $sth = $dbh->prepare($item_level_target_query);
1740 $sth->execute($itemnumber);
1741 my @results;
1742 if ( my $data = $sth->fetchrow_hashref ) {
1743 push( @results, $data );
1745 return @results if @results;
1747 # check for title-level targetted match
1748 my $title_level_target_query = qq/
1749 SELECT reserves.biblionumber AS biblionumber,
1750 reserves.borrowernumber AS borrowernumber,
1751 reserves.reservedate AS reservedate,
1752 reserves.branchcode AS branchcode,
1753 reserves.cancellationdate AS cancellationdate,
1754 reserves.found AS found,
1755 reserves.reservenotes AS reservenotes,
1756 reserves.priority AS priority,
1757 reserves.timestamp AS timestamp,
1758 biblioitems.biblioitemnumber AS biblioitemnumber,
1759 reserves.itemnumber AS itemnumber
1760 FROM reserves
1761 JOIN biblioitems USING (biblionumber)
1762 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1763 WHERE found IS NULL
1764 AND priority > 0
1765 AND item_level_request = 0
1766 AND hold_fill_targets.itemnumber = ?
1767 AND reservedate <= CURRENT_DATE()
1768 AND suspend = 0
1770 $sth = $dbh->prepare($title_level_target_query);
1771 $sth->execute($itemnumber);
1772 @results = ();
1773 if ( my $data = $sth->fetchrow_hashref ) {
1774 push( @results, $data );
1776 return @results if @results;
1778 my $query = qq/
1779 SELECT reserves.biblionumber AS biblionumber,
1780 reserves.borrowernumber AS borrowernumber,
1781 reserves.reservedate AS reservedate,
1782 reserves.waitingdate AS waitingdate,
1783 reserves.branchcode AS branchcode,
1784 reserves.cancellationdate AS cancellationdate,
1785 reserves.found AS found,
1786 reserves.reservenotes AS reservenotes,
1787 reserves.priority AS priority,
1788 reserves.timestamp AS timestamp,
1789 reserveconstraints.biblioitemnumber AS biblioitemnumber,
1790 reserves.itemnumber AS itemnumber
1791 FROM reserves
1792 LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1793 WHERE reserves.biblionumber = ?
1794 AND ( ( reserveconstraints.biblioitemnumber = ?
1795 AND reserves.borrowernumber = reserveconstraints.borrowernumber
1796 AND reserves.reservedate = reserveconstraints.reservedate )
1797 OR reserves.constrainttype='a' )
1798 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1799 AND reserves.reservedate <= CURRENT_DATE()
1800 AND suspend = 0
1802 $sth = $dbh->prepare($query);
1803 $sth->execute( $biblio, $bibitem, $itemnumber );
1804 @results = ();
1805 while ( my $data = $sth->fetchrow_hashref ) {
1806 push( @results, $data );
1808 return @results;
1811 =head2 _koha_notify_reserve
1813 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1815 Sends a notification to the patron that their hold has been filled (through
1816 ModReserveAffect, _not_ ModReserveFill)
1818 =cut
1820 sub _koha_notify_reserve {
1821 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1823 my $dbh = C4::Context->dbh;
1824 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1826 # Try to get the borrower's email address
1827 my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1829 my $letter_code;
1830 my $print_mode = 0;
1831 my $messagingprefs;
1832 if ( $to_address || $borrower->{'smsalertnumber'} ) {
1833 $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } );
1834 } else {
1835 $print_mode = 1;
1838 my $sth = $dbh->prepare("
1839 SELECT *
1840 FROM reserves
1841 WHERE borrowernumber = ?
1842 AND biblionumber = ?
1844 $sth->execute( $borrowernumber, $biblionumber );
1845 my $reserve = $sth->fetchrow_hashref;
1846 my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1848 my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1850 my %letter_params = (
1851 module => 'reserves',
1852 branchcode => $reserve->{branchcode},
1853 tables => {
1854 'branches' => $branch_details,
1855 'borrowers' => $borrower,
1856 'biblio' => $biblionumber,
1857 'reserves' => $reserve,
1858 'items', $reserve->{'itemnumber'},
1860 substitute => { today => C4::Dates->new()->output() },
1864 if ( $print_mode ) {
1865 $letter_params{ 'letter_code' } = 'HOLD_PRINT';
1866 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1868 C4::Letters::EnqueueLetter( {
1869 letter => $letter,
1870 borrowernumber => $borrowernumber,
1871 message_transport_type => 'print',
1872 } );
1874 return;
1877 if ( $to_address && defined $messagingprefs->{transports}->{'email'} ) {
1878 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'email'};
1879 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1881 C4::Letters::EnqueueLetter(
1882 { letter => $letter,
1883 borrowernumber => $borrowernumber,
1884 message_transport_type => 'email',
1885 from_address => $admin_email_address,
1890 if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports}->{'sms'} ) {
1891 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'sms'};
1892 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1894 C4::Letters::EnqueueLetter(
1895 { letter => $letter,
1896 borrowernumber => $borrowernumber,
1897 message_transport_type => 'sms',
1903 =head2 _ShiftPriorityByDateAndPriority
1905 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1907 This increments the priority of all reserves after the one
1908 with either the lowest date after C<$reservedate>
1909 or the lowest priority after C<$priority>.
1911 It effectively makes room for a new reserve to be inserted with a certain
1912 priority, which is returned.
1914 This is most useful when the reservedate can be set by the user. It allows
1915 the new reserve to be placed before other reserves that have a later
1916 reservedate. Since priority also is set by the form in reserves/request.pl
1917 the sub accounts for that too.
1919 =cut
1921 sub _ShiftPriorityByDateAndPriority {
1922 my ( $biblio, $resdate, $new_priority ) = @_;
1924 my $dbh = C4::Context->dbh;
1925 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1926 my $sth = $dbh->prepare( $query );
1927 $sth->execute( $biblio, $resdate, $new_priority );
1928 my $min_priority = $sth->fetchrow;
1929 # if no such matches are found, $new_priority remains as original value
1930 $new_priority = $min_priority if ( $min_priority );
1932 # Shift the priority up by one; works in conjunction with the next SQL statement
1933 $query = "UPDATE reserves
1934 SET priority = priority+1
1935 WHERE biblionumber = ?
1936 AND borrowernumber = ?
1937 AND reservedate = ?
1938 AND found IS NULL";
1939 my $sth_update = $dbh->prepare( $query );
1941 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1942 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1943 $sth = $dbh->prepare( $query );
1944 $sth->execute( $new_priority, $biblio );
1945 while ( my $row = $sth->fetchrow_hashref ) {
1946 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1949 return $new_priority; # so the caller knows what priority they wind up receiving
1952 =head2 MoveReserve
1954 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1956 Use when checking out an item to handle reserves
1957 If $cancelreserve boolean is set to true, it will remove existing reserve
1959 =cut
1961 sub MoveReserve {
1962 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1964 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
1965 return unless $res;
1967 my $biblionumber = $res->{biblionumber};
1968 my $biblioitemnumber = $res->{biblioitemnumber};
1970 if ($res->{borrowernumber} == $borrowernumber) {
1971 ModReserveFill($res);
1973 else {
1974 # warn "Reserved";
1975 # The item is reserved by someone else.
1976 # Find this item in the reserves
1978 my $borr_res;
1979 foreach (@$all_reserves) {
1980 $_->{'borrowernumber'} == $borrowernumber or next;
1981 $_->{'biblionumber'} == $biblionumber or next;
1983 $borr_res = $_;
1984 last;
1987 if ( $borr_res ) {
1988 # The item is reserved by the current patron
1989 ModReserveFill($borr_res);
1992 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1993 RevertWaitingStatus({ itemnumber => $itemnumber });
1995 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1996 CancelReserve({
1997 biblionumber => $res->{'biblionumber'},
1998 itemnumber => $res->{'itemnumber'},
1999 borrowernumber => $res->{'borrowernumber'}
2005 =head2 MergeHolds
2007 MergeHolds($dbh,$to_biblio, $from_biblio);
2009 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2011 =cut
2013 sub MergeHolds {
2014 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2015 my $sth = $dbh->prepare(
2016 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2018 $sth->execute($from_biblio);
2019 if ( my $data = $sth->fetchrow_hashref() ) {
2021 # holds exist on old record, if not we don't need to do anything
2022 $sth = $dbh->prepare(
2023 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2024 $sth->execute( $to_biblio, $from_biblio );
2026 # Reorder by date
2027 # don't reorder those already waiting
2029 $sth = $dbh->prepare(
2030 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2032 my $upd_sth = $dbh->prepare(
2033 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2034 AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2036 $sth->execute( $to_biblio, 'W', 'T' );
2037 my $priority = 1;
2038 while ( my $reserve = $sth->fetchrow_hashref() ) {
2039 $upd_sth->execute(
2040 $priority, $to_biblio,
2041 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2042 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2044 $priority++;
2049 =head2 RevertWaitingStatus
2051 $success = RevertWaitingStatus({ itemnumber => $itemnumber });
2053 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2055 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2056 item level hold, even if it was only a bibliolevel hold to
2057 begin with. This is because we can no longer know if a hold
2058 was item-level or bib-level after a hold has been set to
2059 waiting status.
2061 =cut
2063 sub RevertWaitingStatus {
2064 my ( $params ) = @_;
2065 my $itemnumber = $params->{'itemnumber'};
2067 return unless ( $itemnumber );
2069 my $dbh = C4::Context->dbh;
2071 ## Get the waiting reserve we want to revert
2072 my $query = "
2073 SELECT * FROM reserves
2074 WHERE itemnumber = ?
2075 AND found IS NOT NULL
2077 my $sth = $dbh->prepare( $query );
2078 $sth->execute( $itemnumber );
2079 my $reserve = $sth->fetchrow_hashref();
2081 ## Increment the priority of all other non-waiting
2082 ## reserves for this bib record
2083 $query = "
2084 UPDATE reserves
2086 priority = priority + 1
2087 WHERE
2088 biblionumber = ?
2090 priority > 0
2092 $sth = $dbh->prepare( $query );
2093 $sth->execute( $reserve->{'biblionumber'} );
2095 ## Fix up the currently waiting reserve
2096 $query = "
2097 UPDATE reserves
2099 priority = 1,
2100 found = NULL,
2101 waitingdate = NULL
2102 WHERE
2103 reserve_id = ?
2105 $sth = $dbh->prepare( $query );
2106 return $sth->execute( $reserve->{'reserve_id'} );
2109 =head2 GetReserveId
2111 $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2113 Returnes the first reserve id that matches the given criteria
2115 =cut
2117 sub GetReserveId {
2118 my ( $params ) = @_;
2120 return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2122 my $dbh = C4::Context->dbh();
2124 my $sql = "SELECT reserve_id FROM reserves WHERE ";
2126 my @params;
2127 my @limits;
2128 foreach my $key ( keys %$params ) {
2129 if ( defined( $params->{$key} ) ) {
2130 push( @limits, "$key = ?" );
2131 push( @params, $params->{$key} );
2135 $sql .= join( " AND ", @limits );
2137 my $sth = $dbh->prepare( $sql );
2138 $sth->execute( @params );
2139 my $row = $sth->fetchrow_hashref();
2141 return $row->{'reserve_id'};
2144 =head2 ReserveSlip
2146 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2148 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2150 =cut
2152 sub ReserveSlip {
2153 my ($branch, $borrowernumber, $biblionumber) = @_;
2155 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2157 my $reserve = GetReserveInfo($borrowernumber,$biblionumber )
2158 or return;
2160 return C4::Letters::GetPreparedLetter (
2161 module => 'circulation',
2162 letter_code => 'RESERVESLIP',
2163 branchcode => $branch,
2164 tables => {
2165 'reserves' => $reserve,
2166 'branches' => $reserve->{branchcode},
2167 'borrowers' => $reserve->{borrowernumber},
2168 'biblio' => $reserve->{biblionumber},
2169 'items' => $reserve->{itemnumber},
2174 =head1 AUTHOR
2176 Koha Development Team <http://koha-community.org/>
2178 =cut