Bug 7119 - tidy t/00-load.t
[koha.git] / C4 / Reserves.pm
blob1dc6f72ab8f894facd44e0e746dacbdb12859577
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 &GetReservesFromItemnumber
98 &GetReservesFromBiblionumber
99 &GetReservesFromBorrowernumber
100 &GetReservesForBranch
101 &GetReservesToBranch
102 &GetReserveCount
103 &GetReserveFee
104 &GetReserveInfo
105 &GetReserveStatus
107 &GetOtherReserves
109 &ModReserveFill
110 &ModReserveAffect
111 &ModReserve
112 &ModReserveStatus
113 &ModReserveCancelAll
114 &ModReserveMinusPriority
115 &MoveReserve
117 &CheckReserves
118 &CanBookBeReserved
119 &CanItemBeReserved
120 &CancelReserve
121 &CancelExpiredReserves
123 &AutoUnsuspendReserves
125 &IsAvailableForItemLevelRequest
127 &AlterPriority
128 &ToggleLowestPriority
130 &ReserveSlip
131 &ToggleSuspend
132 &SuspendAll
134 @EXPORT_OK = qw( MergeHolds );
137 =head2 AddReserve
139 AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
141 =cut
143 sub AddReserve {
144 my (
145 $branch, $borrowernumber, $biblionumber,
146 $constraint, $bibitems, $priority, $resdate, $expdate, $notes,
147 $title, $checkitem, $found
148 ) = @_;
149 my $fee =
150 GetReserveFee($borrowernumber, $biblionumber, $constraint,
151 $bibitems );
152 my $dbh = C4::Context->dbh;
153 my $const = lc substr( $constraint, 0, 1 );
154 $resdate = format_date_in_iso( $resdate ) if ( $resdate );
155 $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
156 if ($expdate) {
157 $expdate = format_date_in_iso( $expdate );
158 } else {
159 undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
161 if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
162 # Make room in reserves for this before those of a later reserve date
163 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
165 my $waitingdate;
167 # If the reserv had the waiting status, we had the value of the resdate
168 if ( $found eq 'W' ) {
169 $waitingdate = $resdate;
172 #eval {
173 # updates take place here
174 if ( $fee > 0 ) {
175 my $nextacctno = &getnextacctno( $borrowernumber );
176 my $query = qq/
177 INSERT INTO accountlines
178 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
179 VALUES
180 (?,?,now(),?,?,'Res',?)
182 my $usth = $dbh->prepare($query);
183 $usth->execute( $borrowernumber, $nextacctno, $fee,
184 "Reserve Charge - $title", $fee );
187 #if ($const eq 'a'){
188 my $query = qq/
189 INSERT INTO reserves
190 (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
191 priority,reservenotes,itemnumber,found,waitingdate,expirationdate)
192 VALUES
193 (?,?,?,?,?,
194 ?,?,?,?,?,?)
196 my $sth = $dbh->prepare($query);
197 $sth->execute(
198 $borrowernumber, $biblionumber, $resdate, $branch,
199 $const, $priority, $notes, $checkitem,
200 $found, $waitingdate, $expdate
203 # Send e-mail to librarian if syspref is active
204 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
205 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
206 my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode});
207 if ( my $letter = C4::Letters::GetPreparedLetter (
208 module => 'reserves',
209 letter_code => 'HOLDPLACED',
210 branchcode => $branch,
211 tables => {
212 'branches' => $branch_details,
213 'borrowers' => $borrower,
214 'biblio' => $biblionumber,
216 ) ) {
218 my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
220 C4::Letters::EnqueueLetter(
221 { letter => $letter,
222 borrowernumber => $borrowernumber,
223 message_transport_type => 'email',
224 from_address => $admin_email_address,
225 to_address => $admin_email_address,
232 ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value?
233 $query = qq/
234 INSERT INTO reserveconstraints
235 (borrowernumber,biblionumber,reservedate,biblioitemnumber)
236 VALUES
237 (?,?,?,?)
239 $sth = $dbh->prepare($query); # keep prepare outside the loop!
240 foreach (@$bibitems) {
241 $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
244 return; # FIXME: why not have a useful return value?
247 =head2 GetReservesFromBiblionumber
249 ($count, $title_reserves) = &GetReserves($biblionumber);
251 This function gets the list of reservations for one C<$biblionumber>, returning a count
252 of the reserves and an arrayref pointing to the reserves for C<$biblionumber>.
254 =cut
256 sub GetReservesFromBiblionumber {
257 my ($biblionumber) = shift or return (0, []);
258 my ($all_dates) = shift;
259 my $dbh = C4::Context->dbh;
261 # Find the desired items in the reserves
262 my $query = "
263 SELECT branchcode,
264 timestamp AS rtimestamp,
265 priority,
266 biblionumber,
267 borrowernumber,
268 reservedate,
269 constrainttype,
270 found,
271 itemnumber,
272 reservenotes,
273 expirationdate,
274 lowestPriority,
275 suspend,
276 suspend_until
277 FROM reserves
278 WHERE biblionumber = ? ";
279 unless ( $all_dates ) {
280 $query .= "AND reservedate <= CURRENT_DATE()";
282 $query .= "ORDER BY priority";
283 my $sth = $dbh->prepare($query);
284 $sth->execute($biblionumber);
285 my @results;
286 my $i = 0;
287 while ( my $data = $sth->fetchrow_hashref ) {
289 # FIXME - What is this doing? How do constraints work?
290 if ($data->{constrainttype} eq 'o') {
291 $query = '
292 SELECT biblioitemnumber
293 FROM reserveconstraints
294 WHERE biblionumber = ?
295 AND borrowernumber = ?
296 AND reservedate = ?
298 my $csth = $dbh->prepare($query);
299 $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
300 my @bibitemno;
301 while ( my $bibitemnos = $csth->fetchrow_array ) {
302 push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref
304 my $count = scalar @bibitemno;
306 # if we have two or more different specific itemtypes
307 # reserved by same person on same day
308 my $bdata;
309 if ( $count > 1 ) {
310 $bdata = GetBiblioItemData( $bibitemno[$i] ); # FIXME: This doesn't make sense.
311 $i++; # $i can increase each pass, but the next @bibitemno might be smaller?
313 else {
314 # Look up the book we just found.
315 $bdata = GetBiblioItemData( $bibitemno[0] );
317 # Add the results of this latest search to the current
318 # results.
319 # FIXME - An 'each' would probably be more efficient.
320 foreach my $key ( keys %$bdata ) {
321 $data->{$key} = $bdata->{$key};
324 push @results, $data;
326 return ( $#results + 1, \@results );
329 =head2 GetReservesFromItemnumber
331 ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber);
333 TODO :: Description here
335 =cut
337 sub GetReservesFromItemnumber {
338 my ( $itemnumber, $all_dates ) = @_;
339 my $dbh = C4::Context->dbh;
340 my $query = "
341 SELECT reservedate,borrowernumber,branchcode
342 FROM reserves
343 WHERE itemnumber=?
345 unless ( $all_dates ) {
346 $query .= " AND reservedate <= CURRENT_DATE()";
348 my $sth_res = $dbh->prepare($query);
349 $sth_res->execute($itemnumber);
350 my ( $reservedate, $borrowernumber,$branchcode ) = $sth_res->fetchrow_array;
351 return ( $reservedate, $borrowernumber, $branchcode );
354 =head2 GetReservesFromBorrowernumber
356 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
358 TODO :: Descritpion
360 =cut
362 sub GetReservesFromBorrowernumber {
363 my ( $borrowernumber, $status ) = @_;
364 my $dbh = C4::Context->dbh;
365 my $sth;
366 if ($status) {
367 $sth = $dbh->prepare("
368 SELECT *
369 FROM reserves
370 WHERE borrowernumber=?
371 AND found =?
372 ORDER BY reservedate
374 $sth->execute($borrowernumber,$status);
375 } else {
376 $sth = $dbh->prepare("
377 SELECT *
378 FROM reserves
379 WHERE borrowernumber=?
380 ORDER BY reservedate
382 $sth->execute($borrowernumber);
384 my $data = $sth->fetchall_arrayref({});
385 return @$data;
387 #-------------------------------------------------------------------------------------
388 =head2 CanBookBeReserved
390 $error = &CanBookBeReserved($borrowernumber, $biblionumber)
392 =cut
394 sub CanBookBeReserved{
395 my ($borrowernumber, $biblionumber) = @_;
397 my $items = GetItemnumbersForBiblio($biblionumber);
398 #get items linked via host records
399 my @hostitems = get_hostitemnumbers_of($biblionumber);
400 if (@hostitems){
401 push (@$items,@hostitems);
404 foreach my $item (@$items){
405 return 1 if CanItemBeReserved($borrowernumber, $item);
407 return 0;
410 =head2 CanItemBeReserved
412 $error = &CanItemBeReserved($borrowernumber, $itemnumber)
414 This function return 1 if an item can be issued by this borrower.
416 =cut
418 sub CanItemBeReserved{
419 my ($borrowernumber, $itemnumber) = @_;
421 my $dbh = C4::Context->dbh;
422 my $allowedreserves = 0;
424 my $controlbranch = C4::Context->preference('ReservesControlBranch');
425 my $itype = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype";
427 # we retrieve borrowers and items informations #
428 my $item = GetItem($itemnumber);
429 my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
431 # we retrieve user rights on this itemtype and branchcode
432 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
433 FROM issuingrules
434 WHERE (categorycode in (?,'*') )
435 AND (itemtype IN (?,'*'))
436 AND (branchcode IN (?,'*'))
437 ORDER BY
438 categorycode DESC,
439 itemtype DESC,
440 branchcode DESC;"
443 my $querycount ="SELECT
444 count(*) as count
445 FROM reserves
446 LEFT JOIN items USING (itemnumber)
447 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
448 LEFT JOIN borrowers USING (borrowernumber)
449 WHERE borrowernumber = ?
453 my $itemtype = $item->{$itype};
454 my $categorycode = $borrower->{categorycode};
455 my $branchcode = "";
456 my $branchfield = "reserves.branchcode";
458 if( $controlbranch eq "ItemHomeLibrary" ){
459 $branchfield = "items.homebranch";
460 $branchcode = $item->{homebranch};
461 }elsif( $controlbranch eq "PatronLibrary" ){
462 $branchfield = "borrowers.branchcode";
463 $branchcode = $borrower->{branchcode};
466 # we retrieve rights
467 $sth->execute($categorycode, $itemtype, $branchcode);
468 if(my $rights = $sth->fetchrow_hashref()){
469 $itemtype = $rights->{itemtype};
470 $allowedreserves = $rights->{reservesallowed};
471 }else{
472 $itemtype = '*';
475 # we retrieve count
477 $querycount .= "AND $branchfield = ?";
479 $querycount .= " AND $itype = ?" if ($itemtype ne "*");
480 my $sthcount = $dbh->prepare($querycount);
482 if($itemtype eq "*"){
483 $sthcount->execute($borrowernumber, $branchcode);
484 }else{
485 $sthcount->execute($borrowernumber, $branchcode, $itemtype);
488 my $reservecount = "0";
489 if(my $rowcount = $sthcount->fetchrow_hashref()){
490 $reservecount = $rowcount->{count};
493 # we check if it's ok or not
494 if( $reservecount < $allowedreserves ){
495 return 1;
496 }else{
497 return 0;
500 #--------------------------------------------------------------------------------
501 =head2 GetReserveCount
503 $number = &GetReserveCount($borrowernumber);
505 this function returns the number of reservation for a borrower given on input arg.
507 =cut
509 sub GetReserveCount {
510 my ($borrowernumber) = @_;
512 my $dbh = C4::Context->dbh;
514 my $query = '
515 SELECT COUNT(*) AS counter
516 FROM reserves
517 WHERE borrowernumber = ?
519 my $sth = $dbh->prepare($query);
520 $sth->execute($borrowernumber);
521 my $row = $sth->fetchrow_hashref;
522 return $row->{counter};
525 =head2 GetOtherReserves
527 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
529 Check queued list of this document and check if this document must be transfered
531 =cut
533 sub GetOtherReserves {
534 my ($itemnumber) = @_;
535 my $messages;
536 my $nextreservinfo;
537 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
538 if ($checkreserves) {
539 my $iteminfo = GetItem($itemnumber);
540 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
541 $messages->{'transfert'} = $checkreserves->{'branchcode'};
542 #minus priorities of others reservs
543 ModReserveMinusPriority(
544 $itemnumber,
545 $checkreserves->{'borrowernumber'},
546 $iteminfo->{'biblionumber'}
549 #launch the subroutine dotransfer
550 C4::Items::ModItemTransfer(
551 $itemnumber,
552 $iteminfo->{'holdingbranch'},
553 $checkreserves->{'branchcode'}
558 #step 2b : case of a reservation on the same branch, set the waiting status
559 else {
560 $messages->{'waiting'} = 1;
561 ModReserveMinusPriority(
562 $itemnumber,
563 $checkreserves->{'borrowernumber'},
564 $iteminfo->{'biblionumber'}
566 ModReserveStatus($itemnumber,'W');
569 $nextreservinfo = $checkreserves->{'borrowernumber'};
572 return ( $messages, $nextreservinfo );
575 =head2 GetReserveFee
577 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
579 Calculate the fee for a reserve
581 =cut
583 sub GetReserveFee {
584 my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
586 #check for issues;
587 my $dbh = C4::Context->dbh;
588 my $const = lc substr( $constraint, 0, 1 );
589 my $query = qq/
590 SELECT * FROM borrowers
591 LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
592 WHERE borrowernumber = ?
594 my $sth = $dbh->prepare($query);
595 $sth->execute($borrowernumber);
596 my $data = $sth->fetchrow_hashref;
597 $sth->finish();
598 my $fee = $data->{'reservefee'};
599 my $cntitems = @- > $bibitems;
601 if ( $fee > 0 ) {
603 # check for items on issue
604 # first find biblioitem records
605 my @biblioitems;
606 my $sth1 = $dbh->prepare(
607 "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
608 WHERE (biblio.biblionumber = ?)"
610 $sth1->execute($biblionumber);
611 while ( my $data1 = $sth1->fetchrow_hashref ) {
612 if ( $const eq "a" ) {
613 push @biblioitems, $data1;
615 else {
616 my $found = 0;
617 my $x = 0;
618 while ( $x < $cntitems ) {
619 if ( @$bibitems->{'biblioitemnumber'} ==
620 $data->{'biblioitemnumber'} )
622 $found = 1;
624 $x++;
626 if ( $const eq 'o' ) {
627 if ( $found == 1 ) {
628 push @biblioitems, $data1;
631 else {
632 if ( $found == 0 ) {
633 push @biblioitems, $data1;
638 $sth1->finish;
639 my $cntitemsfound = @biblioitems;
640 my $issues = 0;
641 my $x = 0;
642 my $allissued = 1;
643 while ( $x < $cntitemsfound ) {
644 my $bitdata = $biblioitems[$x];
645 my $sth2 = $dbh->prepare(
646 "SELECT * FROM items
647 WHERE biblioitemnumber = ?"
649 $sth2->execute( $bitdata->{'biblioitemnumber'} );
650 while ( my $itdata = $sth2->fetchrow_hashref ) {
651 my $sth3 = $dbh->prepare(
652 "SELECT * FROM issues
653 WHERE itemnumber = ?"
655 $sth3->execute( $itdata->{'itemnumber'} );
656 if ( my $isdata = $sth3->fetchrow_hashref ) {
658 else {
659 $allissued = 0;
662 $x++;
664 if ( $allissued == 0 ) {
665 my $rsth =
666 $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
667 $rsth->execute($biblionumber);
668 if ( my $rdata = $rsth->fetchrow_hashref ) {
670 else {
671 $fee = 0;
675 return $fee;
678 =head2 GetReservesToBranch
680 @transreserv = GetReservesToBranch( $frombranch );
682 Get reserve list for a given branch
684 =cut
686 sub GetReservesToBranch {
687 my ( $frombranch ) = @_;
688 my $dbh = C4::Context->dbh;
689 my $sth = $dbh->prepare(
690 "SELECT borrowernumber,reservedate,itemnumber,timestamp
691 FROM reserves
692 WHERE priority='0'
693 AND branchcode=?"
695 $sth->execute( $frombranch );
696 my @transreserv;
697 my $i = 0;
698 while ( my $data = $sth->fetchrow_hashref ) {
699 $transreserv[$i] = $data;
700 $i++;
702 return (@transreserv);
705 =head2 GetReservesForBranch
707 @transreserv = GetReservesForBranch($frombranch);
709 =cut
711 sub GetReservesForBranch {
712 my ($frombranch) = @_;
713 my $dbh = C4::Context->dbh;
714 my $query = "SELECT borrowernumber,reservedate,itemnumber,waitingdate
715 FROM reserves
716 WHERE priority='0'
717 AND found='W' ";
718 if ($frombranch){
719 $query .= " AND branchcode=? ";
721 $query .= "ORDER BY waitingdate" ;
722 my $sth = $dbh->prepare($query);
723 if ($frombranch){
724 $sth->execute($frombranch);
726 else {
727 $sth->execute();
729 my @transreserv;
730 my $i = 0;
731 while ( my $data = $sth->fetchrow_hashref ) {
732 $transreserv[$i] = $data;
733 $i++;
735 return (@transreserv);
738 sub GetReserveStatus {
739 my ($itemnumber) = @_;
741 my $dbh = C4::Context->dbh;
743 my $itemstatus = $dbh->prepare("SELECT found FROM reserves WHERE itemnumber = ?");
745 $itemstatus->execute($itemnumber);
746 my ($found) = $itemstatus->fetchrow_array;
747 return $found;
750 =head2 CheckReserves
752 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
753 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
755 Find a book in the reserves.
757 C<$itemnumber> is the book's item number.
759 As I understand it, C<&CheckReserves> looks for the given item in the
760 reserves. If it is found, that's a match, and C<$status> is set to
761 C<Waiting>.
763 Otherwise, it finds the most important item in the reserves with the
764 same biblio number as this book (I'm not clear on this) and returns it
765 with C<$status> set to C<Reserved>.
767 C<&CheckReserves> returns a two-element list:
769 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
771 C<$reserve> is the reserve item that matched. It is a
772 reference-to-hash whose keys are mostly the fields of the reserves
773 table in the Koha database.
775 =cut
777 sub CheckReserves {
778 my ( $item, $barcode ) = @_;
779 my $dbh = C4::Context->dbh;
780 my $sth;
781 my $select;
782 if (C4::Context->preference('item-level_itypes')){
783 $select = "
784 SELECT items.biblionumber,
785 items.biblioitemnumber,
786 itemtypes.notforloan,
787 items.notforloan AS itemnotforloan,
788 items.itemnumber
789 FROM items
790 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
791 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
794 else {
795 $select = "
796 SELECT items.biblionumber,
797 items.biblioitemnumber,
798 itemtypes.notforloan,
799 items.notforloan AS itemnotforloan,
800 items.itemnumber
801 FROM items
802 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
803 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
807 if ($item) {
808 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
809 $sth->execute($item);
811 else {
812 $sth = $dbh->prepare("$select WHERE barcode = ?");
813 $sth->execute($barcode);
815 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
816 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
818 return ( '' ) unless $itemnumber; # bail if we got nothing.
820 # if item is not for loan it cannot be reserved either.....
821 # execpt where items.notforloan < 0 : This indicates the item is holdable.
822 return ( '' ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
824 # Find this item in the reserves
825 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber );
827 # $priority and $highest are used to find the most important item
828 # in the list returned by &_Findgroupreserve. (The lower $priority,
829 # the more important the item.)
830 # $highest is the most important item we've seen so far.
831 my $highest;
832 if (scalar @reserves) {
833 my $priority = 10000000;
834 foreach my $res (@reserves) {
835 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
836 return ( "Waiting", $res, \@reserves ); # Found it
837 } else {
838 # See if this item is more important than what we've got so far
839 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
840 my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'});
841 my $iteminfo=C4::Items::GetItem($itemnumber);
842 my $branch=C4::Circulation::_GetCircControlBranch($iteminfo,$borrowerinfo);
843 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
844 next if ($branchitemrule->{'holdallowed'} == 0);
845 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
846 $priority = $res->{'priority'};
847 $highest = $res;
853 # If we get this far, then no exact match was found.
854 # We return the most important (i.e. next) reservation.
855 if ($highest) {
856 $highest->{'itemnumber'} = $item;
857 return ( "Reserved", $highest, \@reserves );
860 return ( '' );
863 =head2 CancelExpiredReserves
865 CancelExpiredReserves();
867 Cancels all reserves with an expiration date from before today.
869 =cut
871 sub CancelExpiredReserves {
873 # Cancel reserves that have passed their expiration date.
874 my $dbh = C4::Context->dbh;
875 my $sth = $dbh->prepare( "
876 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
877 AND expirationdate IS NOT NULL
878 AND found IS NULL
879 " );
880 $sth->execute();
882 while ( my $res = $sth->fetchrow_hashref() ) {
883 CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
886 # Cancel reserves that have been waiting too long
887 if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
888 my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
889 my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
891 my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
892 $sth = $dbh->prepare( $query );
893 $sth->execute( $max_pickup_delay );
895 while (my $res = $sth->fetchrow_hashref ) {
896 if ( $charge ) {
897 manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
900 CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
906 =head2 AutoUnsuspendReserves
908 AutoUnsuspendReserves();
910 Unsuspends all suspended reserves with a suspend_until date from before today.
912 =cut
914 sub AutoUnsuspendReserves {
916 my $dbh = C4::Context->dbh;
918 my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
919 my $sth = $dbh->prepare( $query );
920 $sth->execute();
924 =head2 CancelReserve
926 &CancelReserve($biblionumber, $itemnumber, $borrowernumber);
928 Cancels a reserve.
930 Use either C<$biblionumber> or C<$itemnumber> to specify the item to
931 cancel, but not both: if both are given, C<&CancelReserve> uses
932 C<$itemnumber>.
934 C<$borrowernumber> is the borrower number of the patron on whose
935 behalf the book was reserved.
937 If C<$biblionumber> was given, C<&CancelReserve> also adjusts the
938 priorities of the other people who are waiting on the book.
940 =cut
942 sub CancelReserve {
943 my ( $biblio, $item, $borr ) = @_;
944 my $dbh = C4::Context->dbh;
945 if ( $item and $borr ) {
946 # removing a waiting reserve record....
947 # update the database...
948 my $query = "
949 UPDATE reserves
950 SET cancellationdate = now(),
951 found = Null,
952 priority = 0
953 WHERE itemnumber = ?
954 AND borrowernumber = ?
956 my $sth = $dbh->prepare($query);
957 $sth->execute( $item, $borr );
958 $sth->finish;
959 $query = "
960 INSERT INTO old_reserves
961 SELECT * FROM reserves
962 WHERE itemnumber = ?
963 AND borrowernumber = ?
965 $sth = $dbh->prepare($query);
966 $sth->execute( $item, $borr );
967 $query = "
968 DELETE FROM reserves
969 WHERE itemnumber = ?
970 AND borrowernumber = ?
972 $sth = $dbh->prepare($query);
973 $sth->execute( $item, $borr );
975 else {
976 # removing a reserve record....
977 # get the prioritiy on this record....
978 my $priority;
979 my $query = qq/
980 SELECT priority FROM reserves
981 WHERE biblionumber = ?
982 AND borrowernumber = ?
983 AND cancellationdate IS NULL
984 AND itemnumber IS NULL
986 my $sth = $dbh->prepare($query);
987 $sth->execute( $biblio, $borr );
988 ($priority) = $sth->fetchrow_array;
989 $sth->finish;
990 $query = qq/
991 UPDATE reserves
992 SET cancellationdate = now(),
993 found = Null,
994 priority = 0
995 WHERE biblionumber = ?
996 AND borrowernumber = ?
999 # update the database, removing the record...
1000 $sth = $dbh->prepare($query);
1001 $sth->execute( $biblio, $borr );
1002 $sth->finish;
1004 $query = qq/
1005 INSERT INTO old_reserves
1006 SELECT * FROM reserves
1007 WHERE biblionumber = ?
1008 AND borrowernumber = ?
1010 $sth = $dbh->prepare($query);
1011 $sth->execute( $biblio, $borr );
1013 $query = qq/
1014 DELETE FROM reserves
1015 WHERE biblionumber = ?
1016 AND borrowernumber = ?
1018 $sth = $dbh->prepare($query);
1019 $sth->execute( $biblio, $borr );
1021 # now fix the priority on the others....
1022 _FixPriority( $biblio, $borr );
1026 =head2 ModReserve
1028 ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber])
1030 Change a hold request's priority or cancel it.
1032 C<$rank> specifies the effect of the change. If C<$rank>
1033 is 'W' or 'n', nothing happens. This corresponds to leaving a
1034 request alone when changing its priority in the holds queue
1035 for a bib.
1037 If C<$rank> is 'del', the hold request is cancelled.
1039 If C<$rank> is an integer greater than zero, the priority of
1040 the request is set to that value. Since priority != 0 means
1041 that the item is not waiting on the hold shelf, setting the
1042 priority to a non-zero value also sets the request's found
1043 status and waiting date to NULL.
1045 The optional C<$itemnumber> parameter is used only when
1046 C<$rank> is a non-zero integer; if supplied, the itemnumber
1047 of the hold request is set accordingly; if omitted, the itemnumber
1048 is cleared.
1050 B<FIXME:> Note that the forgoing can have the effect of causing
1051 item-level hold requests to turn into title-level requests. This
1052 will be fixed once reserves has separate columns for requested
1053 itemnumber and supplying itemnumber.
1055 =cut
1057 sub ModReserve {
1058 #subroutine to update a reserve
1059 my ( $rank, $biblio, $borrower, $branch , $itemnumber, $suspend_until) = @_;
1060 return if $rank eq "W";
1061 return if $rank eq "n";
1062 my $dbh = C4::Context->dbh;
1063 if ( $rank eq "del" ) {
1064 my $query = qq/
1065 UPDATE reserves
1066 SET cancellationdate=now()
1067 WHERE biblionumber = ?
1068 AND borrowernumber = ?
1070 my $sth = $dbh->prepare($query);
1071 $sth->execute( $biblio, $borrower );
1072 $sth->finish;
1073 $query = qq/
1074 INSERT INTO old_reserves
1075 SELECT *
1076 FROM reserves
1077 WHERE biblionumber = ?
1078 AND borrowernumber = ?
1080 $sth = $dbh->prepare($query);
1081 $sth->execute( $biblio, $borrower );
1082 $query = qq/
1083 DELETE FROM reserves
1084 WHERE biblionumber = ?
1085 AND borrowernumber = ?
1087 $sth = $dbh->prepare($query);
1088 $sth->execute( $biblio, $borrower );
1091 elsif ($rank =~ /^\d+/ and $rank > 0) {
1092 my $query = "
1093 UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1094 WHERE biblionumber = ?
1095 AND borrowernumber = ?
1097 my $sth = $dbh->prepare($query);
1098 $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower);
1099 $sth->finish;
1101 if ( defined( $suspend_until ) ) {
1102 if ( $suspend_until ) {
1103 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1104 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $suspend_until, $biblio, $borrower ) );
1105 } else {
1106 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $biblio, $borrower ) );
1110 _FixPriority( $biblio, $borrower, $rank);
1114 =head2 ModReserveFill
1116 &ModReserveFill($reserve);
1118 Fill a reserve. If I understand this correctly, this means that the
1119 reserved book has been found and given to the patron who reserved it.
1121 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1122 whose keys are fields from the reserves table in the Koha database.
1124 =cut
1126 sub ModReserveFill {
1127 my ($res) = @_;
1128 my $dbh = C4::Context->dbh;
1129 # fill in a reserve record....
1130 my $biblionumber = $res->{'biblionumber'};
1131 my $borrowernumber = $res->{'borrowernumber'};
1132 my $resdate = $res->{'reservedate'};
1134 # get the priority on this record....
1135 my $priority;
1136 my $query = "SELECT priority
1137 FROM reserves
1138 WHERE biblionumber = ?
1139 AND borrowernumber = ?
1140 AND reservedate = ?";
1141 my $sth = $dbh->prepare($query);
1142 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1143 ($priority) = $sth->fetchrow_array;
1144 $sth->finish;
1146 # update the database...
1147 $query = "UPDATE reserves
1148 SET found = 'F',
1149 priority = 0
1150 WHERE biblionumber = ?
1151 AND reservedate = ?
1152 AND borrowernumber = ?
1154 $sth = $dbh->prepare($query);
1155 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1156 $sth->finish;
1158 # move to old_reserves
1159 $query = "INSERT INTO old_reserves
1160 SELECT * FROM reserves
1161 WHERE biblionumber = ?
1162 AND reservedate = ?
1163 AND borrowernumber = ?
1165 $sth = $dbh->prepare($query);
1166 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1167 $query = "DELETE FROM reserves
1168 WHERE biblionumber = ?
1169 AND reservedate = ?
1170 AND borrowernumber = ?
1172 $sth = $dbh->prepare($query);
1173 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1175 # now fix the priority on the others (if the priority wasn't
1176 # already sorted!)....
1177 unless ( $priority == 0 ) {
1178 _FixPriority( $biblionumber, $borrowernumber );
1182 =head2 ModReserveStatus
1184 &ModReserveStatus($itemnumber, $newstatus);
1186 Update the reserve status for the active (priority=0) reserve.
1188 $itemnumber is the itemnumber the reserve is on
1190 $newstatus is the new status.
1192 =cut
1194 sub ModReserveStatus {
1196 #first : check if we have a reservation for this item .
1197 my ($itemnumber, $newstatus) = @_;
1198 my $dbh = C4::Context->dbh;
1200 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1201 my $sth_set = $dbh->prepare($query);
1202 $sth_set->execute( $newstatus, $itemnumber );
1204 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1205 CartToShelf( $itemnumber );
1209 =head2 ModReserveAffect
1211 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1213 This function affect an item and a status for a given reserve
1214 The itemnumber parameter is used to find the biblionumber.
1215 with the biblionumber & the borrowernumber, we can affect the itemnumber
1216 to the correct reserve.
1218 if $transferToDo is not set, then the status is set to "Waiting" as well.
1219 otherwise, a transfer is on the way, and the end of the transfer will
1220 take care of the waiting status
1222 =cut
1224 sub ModReserveAffect {
1225 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1226 my $dbh = C4::Context->dbh;
1228 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1229 # attached to $itemnumber
1230 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1231 $sth->execute($itemnumber);
1232 my ($biblionumber) = $sth->fetchrow;
1234 # get request - need to find out if item is already
1235 # waiting in order to not send duplicate hold filled notifications
1236 my $request = GetReserveInfo($borrowernumber, $biblionumber);
1237 my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1239 # If we affect a reserve that has to be transfered, don't set to Waiting
1240 my $query;
1241 if ($transferToDo) {
1242 $query = "
1243 UPDATE reserves
1244 SET priority = 0,
1245 itemnumber = ?,
1246 found = 'T'
1247 WHERE borrowernumber = ?
1248 AND biblionumber = ?
1251 else {
1252 # affect the reserve to Waiting as well.
1253 $query = "
1254 UPDATE reserves
1255 SET priority = 0,
1256 found = 'W',
1257 waitingdate = NOW(),
1258 itemnumber = ?
1259 WHERE borrowernumber = ?
1260 AND biblionumber = ?
1263 $sth = $dbh->prepare($query);
1264 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1265 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1267 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1268 CartToShelf( $itemnumber );
1271 return;
1274 =head2 ModReserveCancelAll
1276 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1278 function to cancel reserv,check other reserves, and transfer document if it's necessary
1280 =cut
1282 sub ModReserveCancelAll {
1283 my $messages;
1284 my $nextreservinfo;
1285 my ( $itemnumber, $borrowernumber ) = @_;
1287 #step 1 : cancel the reservation
1288 my $CancelReserve = CancelReserve( undef, $itemnumber, $borrowernumber );
1290 #step 2 launch the subroutine of the others reserves
1291 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1293 return ( $messages, $nextreservinfo );
1296 =head2 ModReserveMinusPriority
1298 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1300 Reduce the values of queuded list
1302 =cut
1304 sub ModReserveMinusPriority {
1305 my ( $itemnumber, $borrowernumber, $biblionumber ) = @_;
1307 #first step update the value of the first person on reserv
1308 my $dbh = C4::Context->dbh;
1309 my $query = "
1310 UPDATE reserves
1311 SET priority = 0 , itemnumber = ?
1312 WHERE borrowernumber=?
1313 AND biblionumber=?
1315 my $sth_upd = $dbh->prepare($query);
1316 $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber );
1317 # second step update all others reservs
1318 _FixPriority($biblionumber, $borrowernumber, '0');
1321 =head2 GetReserveInfo
1323 &GetReserveInfo($borrowernumber,$biblionumber);
1325 Get item and borrower details for a current hold.
1326 Current implementation this query should have a single result.
1328 =cut
1330 sub GetReserveInfo {
1331 my ( $borrowernumber, $biblionumber ) = @_;
1332 my $dbh = C4::Context->dbh;
1333 my $strsth="SELECT
1334 reservedate,
1335 reservenotes,
1336 reserves.borrowernumber,
1337 reserves.biblionumber,
1338 reserves.branchcode,
1339 reserves.waitingdate,
1340 notificationdate,
1341 reminderdate,
1342 priority,
1343 found,
1344 firstname,
1345 surname,
1346 phone,
1347 email,
1348 address,
1349 address2,
1350 cardnumber,
1351 city,
1352 zipcode,
1353 biblio.title,
1354 biblio.author,
1355 items.holdingbranch,
1356 items.itemcallnumber,
1357 items.itemnumber,
1358 items.location,
1359 barcode,
1360 notes
1361 FROM reserves
1362 LEFT JOIN items USING(itemnumber)
1363 LEFT JOIN borrowers USING(borrowernumber)
1364 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1365 WHERE
1366 reserves.borrowernumber=?
1367 AND reserves.biblionumber=?";
1368 my $sth = $dbh->prepare($strsth);
1369 $sth->execute($borrowernumber,$biblionumber);
1371 my $data = $sth->fetchrow_hashref;
1372 return $data;
1376 =head2 IsAvailableForItemLevelRequest
1378 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1380 Checks whether a given item record is available for an
1381 item-level hold request. An item is available if
1383 * it is not lost AND
1384 * it is not damaged AND
1385 * it is not withdrawn AND
1386 * does not have a not for loan value > 0
1388 Whether or not the item is currently on loan is
1389 also checked - if the AllowOnShelfHolds system preference
1390 is ON, an item can be requested even if it is currently
1391 on loan to somebody else. If the system preference
1392 is OFF, an item that is currently checked out cannot
1393 be the target of an item-level hold request.
1395 Note that IsAvailableForItemLevelRequest() does not
1396 check if the staff operator is authorized to place
1397 a request on the item - in particular,
1398 this routine does not check IndependantBranches
1399 and canreservefromotherbranches.
1401 =cut
1403 sub IsAvailableForItemLevelRequest {
1404 my $itemnumber = shift;
1406 my $item = GetItem($itemnumber);
1408 # must check the notforloan setting of the itemtype
1409 # FIXME - a lot of places in the code do this
1410 # or something similar - need to be
1411 # consolidated
1412 my $dbh = C4::Context->dbh;
1413 my $notforloan_query;
1414 if (C4::Context->preference('item-level_itypes')) {
1415 $notforloan_query = "SELECT itemtypes.notforloan
1416 FROM items
1417 JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1418 WHERE itemnumber = ?";
1419 } else {
1420 $notforloan_query = "SELECT itemtypes.notforloan
1421 FROM items
1422 JOIN biblioitems USING (biblioitemnumber)
1423 JOIN itemtypes USING (itemtype)
1424 WHERE itemnumber = ?";
1426 my $sth = $dbh->prepare($notforloan_query);
1427 $sth->execute($itemnumber);
1428 my $notforloan_per_itemtype = 0;
1429 if (my ($notforloan) = $sth->fetchrow_array) {
1430 $notforloan_per_itemtype = 1 if $notforloan;
1433 my $available_per_item = 1;
1434 $available_per_item = 0 if $item->{itemlost} or
1435 ( $item->{notforloan} > 0 ) or
1436 ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1437 $item->{wthdrawn} or
1438 $notforloan_per_itemtype;
1441 if (C4::Context->preference('AllowOnShelfHolds')) {
1442 return $available_per_item;
1443 } else {
1444 return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "W"));
1448 =head2 AlterPriority
1450 AlterPriority( $where, $borrowernumber, $biblionumber, $reservedate );
1452 This function changes a reserve's priority up, down, to the top, or to the bottom.
1453 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1455 =cut
1457 sub AlterPriority {
1458 my ( $where, $borrowernumber, $biblionumber ) = @_;
1460 my $dbh = C4::Context->dbh;
1462 ## Find this reserve
1463 my $sth = $dbh->prepare('SELECT * FROM reserves WHERE biblionumber = ? AND borrowernumber = ? AND cancellationdate IS NULL');
1464 $sth->execute( $biblionumber, $borrowernumber );
1465 my $reserve = $sth->fetchrow_hashref();
1466 $sth->finish();
1468 if ( $where eq 'up' || $where eq 'down' ) {
1470 my $priority = $reserve->{'priority'};
1471 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1472 _FixPriority( $biblionumber, $borrowernumber, $priority )
1474 } elsif ( $where eq 'top' ) {
1476 _FixPriority( $biblionumber, $borrowernumber, '1' )
1478 } elsif ( $where eq 'bottom' ) {
1480 _FixPriority( $biblionumber, $borrowernumber, '999999' )
1485 =head2 ToggleLowestPriority
1487 ToggleLowestPriority( $borrowernumber, $biblionumber );
1489 This function sets the lowestPriority field to true if is false, and false if it is true.
1491 =cut
1493 sub ToggleLowestPriority {
1494 my ( $borrowernumber, $biblionumber ) = @_;
1496 my $dbh = C4::Context->dbh;
1498 my $sth = $dbh->prepare(
1499 "UPDATE reserves SET lowestPriority = NOT lowestPriority
1500 WHERE biblionumber = ?
1501 AND borrowernumber = ?"
1503 $sth->execute(
1504 $biblionumber,
1505 $borrowernumber,
1507 $sth->finish;
1509 _FixPriority( $biblionumber, $borrowernumber, '999999' );
1512 =head2 ToggleSuspend
1514 ToggleSuspend( $borrowernumber, $biblionumber );
1516 This function sets the suspend field to true if is false, and false if it is true.
1517 If the reserve is currently suspended with a suspend_until date, that date will
1518 be cleared when it is unsuspended.
1520 =cut
1522 sub ToggleSuspend {
1523 my ( $borrowernumber, $biblionumber, $suspend_until ) = @_;
1525 $suspend_until = output_pref( dt_from_string( $suspend_until ), 'iso' ) if ( $suspend_until );
1527 my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1529 my $dbh = C4::Context->dbh;
1531 my $sth = $dbh->prepare(
1532 "UPDATE reserves SET suspend = NOT suspend,
1533 suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1534 WHERE biblionumber = ?
1535 AND borrowernumber = ?
1538 my @params;
1539 push( @params, $suspend_until ) if ( $suspend_until );
1540 push( @params, $biblionumber );
1541 push( @params, $borrowernumber );
1543 $sth->execute( @params );
1544 $sth->finish;
1547 =head2 SuspendAll
1549 SuspendAll(
1550 borrowernumber => $borrowernumber,
1551 [ biblionumber => $biblionumber, ]
1552 [ suspend_until => $suspend_until, ]
1553 [ suspend => $suspend ]
1556 This function accepts a set of hash keys as its parameters.
1557 It requires either borrowernumber or biblionumber, or both.
1559 suspend_until is wholly optional.
1561 =cut
1563 sub SuspendAll {
1564 my %params = @_;
1566 my $borrowernumber = $params{'borrowernumber'} || undef;
1567 my $biblionumber = $params{'biblionumber'} || undef;
1568 my $suspend_until = $params{'suspend_until'} || undef;
1569 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1571 $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1573 return unless ( $borrowernumber || $biblionumber );
1575 my ( $query, $sth, $dbh, @query_params );
1577 $query = "UPDATE reserves SET suspend = ? ";
1578 push( @query_params, $suspend );
1579 if ( !$suspend ) {
1580 $query .= ", suspend_until = NULL ";
1581 } elsif ( $suspend_until ) {
1582 $query .= ", suspend_until = ? ";
1583 push( @query_params, $suspend_until );
1585 $query .= " WHERE ";
1586 if ( $borrowernumber ) {
1587 $query .= " borrowernumber = ? ";
1588 push( @query_params, $borrowernumber );
1590 $query .= " AND " if ( $borrowernumber && $biblionumber );
1591 if ( $biblionumber ) {
1592 $query .= " biblionumber = ? ";
1593 push( @query_params, $biblionumber );
1595 $query .= " AND found IS NULL ";
1597 $dbh = C4::Context->dbh;
1598 $sth = $dbh->prepare( $query );
1599 $sth->execute( @query_params );
1600 $sth->finish;
1604 =head2 _FixPriority
1606 &_FixPriority($biblio,$borrowernumber,$rank,$ignoreSetLowestRank);
1608 Only used internally (so don't export it)
1609 Changed how this functions works #
1610 Now just gets an array of reserves in the rank order and updates them with
1611 the array index (+1 as array starts from 0)
1612 and if $rank is supplied will splice item from the array and splice it back in again
1613 in new priority rank
1615 =cut
1617 sub _FixPriority {
1618 my ( $biblio, $borrowernumber, $rank, $ignoreSetLowestRank ) = @_;
1619 my $dbh = C4::Context->dbh;
1620 if ( $rank eq "del" ) {
1621 CancelReserve( $biblio, undef, $borrowernumber );
1623 if ( $rank eq "W" || $rank eq "0" ) {
1625 # make sure priority for waiting or in-transit items is 0
1626 my $query = qq/
1627 UPDATE reserves
1628 SET priority = 0
1629 WHERE biblionumber = ?
1630 AND borrowernumber = ?
1631 AND found IN ('W', 'T')
1633 my $sth = $dbh->prepare($query);
1634 $sth->execute( $biblio, $borrowernumber );
1636 my @priority;
1637 my @reservedates;
1639 # get whats left
1640 # FIXME adding a new security in returned elements for changing priority,
1641 # now, we don't care anymore any reservations with itemnumber linked (suppose a waiting reserve)
1642 # This is wrong a waiting reserve has W set
1643 # The assumption that having an itemnumber set means waiting is wrong and should be corrected any place it occurs
1644 my $query = qq/
1645 SELECT borrowernumber, reservedate, constrainttype
1646 FROM reserves
1647 WHERE biblionumber = ?
1648 AND ((found <> 'W' AND found <> 'T') or found is NULL)
1649 ORDER BY priority ASC
1651 my $sth = $dbh->prepare($query);
1652 $sth->execute($biblio);
1653 while ( my $line = $sth->fetchrow_hashref ) {
1654 push( @reservedates, $line );
1655 push( @priority, $line );
1658 # To find the matching index
1659 my $i;
1660 my $key = -1; # to allow for 0 to be a valid result
1661 for ( $i = 0 ; $i < @priority ; $i++ ) {
1662 if ( $borrowernumber == $priority[$i]->{'borrowernumber'} ) {
1663 $key = $i; # save the index
1664 last;
1668 # if index exists in array then move it to new position
1669 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1670 my $new_rank = $rank -
1671 1; # $new_rank is what you want the new index to be in the array
1672 my $moving_item = splice( @priority, $key, 1 );
1673 splice( @priority, $new_rank, 0, $moving_item );
1676 # now fix the priority on those that are left....
1677 $query = "
1678 UPDATE reserves
1679 SET priority = ?
1680 WHERE biblionumber = ?
1681 AND borrowernumber = ?
1682 AND reservedate = ?
1683 AND found IS NULL
1685 $sth = $dbh->prepare($query);
1686 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1687 $sth->execute(
1688 $j + 1, $biblio,
1689 $priority[$j]->{'borrowernumber'},
1690 $priority[$j]->{'reservedate'}
1692 $sth->finish;
1695 $sth = $dbh->prepare( "SELECT borrowernumber FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1696 $sth->execute();
1698 unless ( $ignoreSetLowestRank ) {
1699 while ( my $res = $sth->fetchrow_hashref() ) {
1700 _FixPriority( $biblio, $res->{'borrowernumber'}, '999999', 1 );
1705 =head2 _Findgroupreserve
1707 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1709 Looks for an item-specific match first, then for a title-level match, returning the
1710 first match found. If neither, then we look for a 3rd kind of match based on
1711 reserve constraints.
1713 TODO: add more explanation about reserve constraints
1715 C<&_Findgroupreserve> returns :
1716 C<@results> is an array of references-to-hash whose keys are mostly
1717 fields from the reserves table of the Koha database, plus
1718 C<biblioitemnumber>.
1720 =cut
1722 sub _Findgroupreserve {
1723 my ( $bibitem, $biblio, $itemnumber ) = @_;
1724 my $dbh = C4::Context->dbh;
1726 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1727 # check for exact targetted match
1728 my $item_level_target_query = qq/
1729 SELECT reserves.biblionumber AS biblionumber,
1730 reserves.borrowernumber AS borrowernumber,
1731 reserves.reservedate AS reservedate,
1732 reserves.branchcode AS branchcode,
1733 reserves.cancellationdate AS cancellationdate,
1734 reserves.found AS found,
1735 reserves.reservenotes AS reservenotes,
1736 reserves.priority AS priority,
1737 reserves.timestamp AS timestamp,
1738 biblioitems.biblioitemnumber AS biblioitemnumber,
1739 reserves.itemnumber AS itemnumber
1740 FROM reserves
1741 JOIN biblioitems USING (biblionumber)
1742 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1743 WHERE found IS NULL
1744 AND priority > 0
1745 AND item_level_request = 1
1746 AND itemnumber = ?
1747 AND reservedate <= CURRENT_DATE()
1748 AND suspend = 0
1750 my $sth = $dbh->prepare($item_level_target_query);
1751 $sth->execute($itemnumber);
1752 my @results;
1753 if ( my $data = $sth->fetchrow_hashref ) {
1754 push( @results, $data );
1756 return @results if @results;
1758 # check for title-level targetted match
1759 my $title_level_target_query = qq/
1760 SELECT reserves.biblionumber AS biblionumber,
1761 reserves.borrowernumber AS borrowernumber,
1762 reserves.reservedate AS reservedate,
1763 reserves.branchcode AS branchcode,
1764 reserves.cancellationdate AS cancellationdate,
1765 reserves.found AS found,
1766 reserves.reservenotes AS reservenotes,
1767 reserves.priority AS priority,
1768 reserves.timestamp AS timestamp,
1769 biblioitems.biblioitemnumber AS biblioitemnumber,
1770 reserves.itemnumber AS itemnumber
1771 FROM reserves
1772 JOIN biblioitems USING (biblionumber)
1773 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1774 WHERE found IS NULL
1775 AND priority > 0
1776 AND item_level_request = 0
1777 AND hold_fill_targets.itemnumber = ?
1778 AND reservedate <= CURRENT_DATE()
1779 AND suspend = 0
1781 $sth = $dbh->prepare($title_level_target_query);
1782 $sth->execute($itemnumber);
1783 @results = ();
1784 if ( my $data = $sth->fetchrow_hashref ) {
1785 push( @results, $data );
1787 return @results if @results;
1789 my $query = qq/
1790 SELECT reserves.biblionumber AS biblionumber,
1791 reserves.borrowernumber AS borrowernumber,
1792 reserves.reservedate AS reservedate,
1793 reserves.waitingdate AS waitingdate,
1794 reserves.branchcode AS branchcode,
1795 reserves.cancellationdate AS cancellationdate,
1796 reserves.found AS found,
1797 reserves.reservenotes AS reservenotes,
1798 reserves.priority AS priority,
1799 reserves.timestamp AS timestamp,
1800 reserveconstraints.biblioitemnumber AS biblioitemnumber,
1801 reserves.itemnumber AS itemnumber
1802 FROM reserves
1803 LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1804 WHERE reserves.biblionumber = ?
1805 AND ( ( reserveconstraints.biblioitemnumber = ?
1806 AND reserves.borrowernumber = reserveconstraints.borrowernumber
1807 AND reserves.reservedate = reserveconstraints.reservedate )
1808 OR reserves.constrainttype='a' )
1809 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1810 AND reserves.reservedate <= CURRENT_DATE()
1811 AND suspend = 0
1813 $sth = $dbh->prepare($query);
1814 $sth->execute( $biblio, $bibitem, $itemnumber );
1815 @results = ();
1816 while ( my $data = $sth->fetchrow_hashref ) {
1817 push( @results, $data );
1819 return @results;
1822 =head2 _koha_notify_reserve
1824 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1826 Sends a notification to the patron that their hold has been filled (through
1827 ModReserveAffect, _not_ ModReserveFill)
1829 =cut
1831 sub _koha_notify_reserve {
1832 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1834 my $dbh = C4::Context->dbh;
1835 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1837 # Try to get the borrower's email address
1838 my $to_address;
1839 my $which_address = C4::Context->preference('AutoEmailPrimaryAddress');
1840 # If the system preference is set to 'first valid' (value == OFF), look up email address
1841 if ($which_address eq 'OFF') {
1842 $to_address = C4::Members::GetFirstValidEmailAddress( $borrowernumber );
1843 } else {
1844 $to_address = $borrower->{$which_address};
1847 my $letter_code;
1848 my $print_mode = 0;
1849 my $messagingprefs;
1850 if ( $to_address || $borrower->{'smsalertnumber'} ) {
1851 $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } );
1852 } else {
1853 $print_mode = 1;
1856 my $sth = $dbh->prepare("
1857 SELECT *
1858 FROM reserves
1859 WHERE borrowernumber = ?
1860 AND biblionumber = ?
1862 $sth->execute( $borrowernumber, $biblionumber );
1863 my $reserve = $sth->fetchrow_hashref;
1864 my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1866 my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1868 my %letter_params = (
1869 module => 'reserves',
1870 branchcode => $reserve->{branchcode},
1871 tables => {
1872 'branches' => $branch_details,
1873 'borrowers' => $borrower,
1874 'biblio' => $biblionumber,
1875 'reserves' => $reserve,
1876 'items', $reserve->{'itemnumber'},
1878 substitute => { today => C4::Dates->new()->output() },
1882 if ( $print_mode ) {
1883 $letter_params{ 'letter_code' } = 'HOLD_PRINT';
1884 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1886 C4::Letters::EnqueueLetter( {
1887 letter => $letter,
1888 borrowernumber => $borrowernumber,
1889 message_transport_type => 'print',
1890 } );
1892 return;
1895 if ( $to_address && defined $messagingprefs->{transports}->{'email'} ) {
1896 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'email'};
1897 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1899 C4::Letters::EnqueueLetter(
1900 { letter => $letter,
1901 borrowernumber => $borrowernumber,
1902 message_transport_type => 'email',
1903 from_address => $admin_email_address,
1908 if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports}->{'sms'} ) {
1909 $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'sms'};
1910 my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1912 C4::Letters::EnqueueLetter(
1913 { letter => $letter,
1914 borrowernumber => $borrowernumber,
1915 message_transport_type => 'sms',
1921 =head2 _ShiftPriorityByDateAndPriority
1923 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1925 This increments the priority of all reserves after the one
1926 with either the lowest date after C<$reservedate>
1927 or the lowest priority after C<$priority>.
1929 It effectively makes room for a new reserve to be inserted with a certain
1930 priority, which is returned.
1932 This is most useful when the reservedate can be set by the user. It allows
1933 the new reserve to be placed before other reserves that have a later
1934 reservedate. Since priority also is set by the form in reserves/request.pl
1935 the sub accounts for that too.
1937 =cut
1939 sub _ShiftPriorityByDateAndPriority {
1940 my ( $biblio, $resdate, $new_priority ) = @_;
1942 my $dbh = C4::Context->dbh;
1943 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1944 my $sth = $dbh->prepare( $query );
1945 $sth->execute( $biblio, $resdate, $new_priority );
1946 my $min_priority = $sth->fetchrow;
1947 # if no such matches are found, $new_priority remains as original value
1948 $new_priority = $min_priority if ( $min_priority );
1950 # Shift the priority up by one; works in conjunction with the next SQL statement
1951 $query = "UPDATE reserves
1952 SET priority = priority+1
1953 WHERE biblionumber = ?
1954 AND borrowernumber = ?
1955 AND reservedate = ?
1956 AND found IS NULL";
1957 my $sth_update = $dbh->prepare( $query );
1959 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1960 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1961 $sth = $dbh->prepare( $query );
1962 $sth->execute( $new_priority, $biblio );
1963 while ( my $row = $sth->fetchrow_hashref ) {
1964 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1967 return $new_priority; # so the caller knows what priority they wind up receiving
1970 =head2 MoveReserve
1972 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1974 Use when checking out an item to handle reserves
1975 If $cancelreserve boolean is set to true, it will remove existing reserve
1977 =cut
1979 sub MoveReserve {
1980 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1982 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
1983 return unless $res;
1985 my $biblionumber = $res->{biblionumber};
1986 my $biblioitemnumber = $res->{biblioitemnumber};
1988 if ($res->{borrowernumber} == $borrowernumber) {
1989 ModReserveFill($res);
1991 else {
1992 # warn "Reserved";
1993 # The item is reserved by someone else.
1994 # Find this item in the reserves
1996 my $borr_res;
1997 foreach (@$all_reserves) {
1998 $_->{'borrowernumber'} == $borrowernumber or next;
1999 $_->{'biblionumber'} == $biblionumber or next;
2001 $borr_res = $_;
2002 last;
2005 if ( $borr_res ) {
2006 # The item is reserved by the current patron
2007 ModReserveFill($borr_res);
2010 if ($cancelreserve) { # cancel reserves on this item
2011 CancelReserve(0, $res->{'itemnumber'}, $res->{'borrowernumber'});
2012 CancelReserve($res->{'biblionumber'}, 0, $res->{'borrowernumber'});
2017 =head2 MergeHolds
2019 MergeHolds($dbh,$to_biblio, $from_biblio);
2021 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2023 =cut
2025 sub MergeHolds {
2026 my ( $dbh, $to_biblio, $from_biblio ) = @_;
2027 my $sth = $dbh->prepare(
2028 "SELECT count(*) as reservenumber FROM reserves WHERE biblionumber = ?"
2030 $sth->execute($from_biblio);
2031 if ( my $data = $sth->fetchrow_hashref() ) {
2033 # holds exist on old record, if not we don't need to do anything
2034 $sth = $dbh->prepare(
2035 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2036 $sth->execute( $to_biblio, $from_biblio );
2038 # Reorder by date
2039 # don't reorder those already waiting
2041 $sth = $dbh->prepare(
2042 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2044 my $upd_sth = $dbh->prepare(
2045 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2046 AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2048 $sth->execute( $to_biblio, 'W', 'T' );
2049 my $priority = 1;
2050 while ( my $reserve = $sth->fetchrow_hashref() ) {
2051 $upd_sth->execute(
2052 $priority, $to_biblio,
2053 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2054 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2056 $priority++;
2061 =head2 ReserveSlip
2063 ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2065 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2067 =cut
2069 sub ReserveSlip {
2070 my ($branch, $borrowernumber, $biblionumber) = @_;
2072 # return unless ( C4::Context->boolean_preference('printreserveslips') );
2074 my $reserve = GetReserveInfo($borrowernumber,$biblionumber )
2075 or return;
2077 return C4::Letters::GetPreparedLetter (
2078 module => 'circulation',
2079 letter_code => 'RESERVESLIP',
2080 branchcode => $branch,
2081 tables => {
2082 'reserves' => $reserve,
2083 'branches' => $reserve->{branchcode},
2084 'borrowers' => $reserve->{borrowernumber},
2085 'biblio' => $reserve->{biblionumber},
2086 'items' => $reserve->{itemnumber},
2091 =head1 AUTHOR
2093 Koha Development Team <http://koha-community.org/>
2095 =cut