Bug 3212 Force leader 9 position to 'a' for new biblios
[koha.git] / C4 / Reserves.pm
blob2dd2688fcdc454dc75bd2a2d2539aa5547fc9d32
1 package C4::Reserves;
3 # Copyright 2000-2002 Katipo Communications
4 # 2006 SAN Ouest Provence
5 # 2007 BibLibre Paul POULAIN
7 # This file is part of Koha.
9 # Koha is free software; you can redistribute it and/or modify it under the
10 # terms of the GNU General Public License as published by the Free Software
11 # Foundation; either version 2 of the License, or (at your option) any later
12 # version.
14 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License along
19 # with Koha; if not, write to the Free Software Foundation, Inc.,
20 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 use strict;
24 #use warnings; FIXME - Bug 2505
25 use C4::Context;
26 use C4::Biblio;
27 use C4::Members;
28 use C4::Items;
29 use C4::Search;
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 );
39 use List::MoreUtils qw( firstidx );
41 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
43 =head1 NAME
45 C4::Reserves - Koha functions for dealing with reservation.
47 =head1 SYNOPSIS
49 use C4::Reserves;
51 =head1 DESCRIPTION
53 This modules provides somes functions to deal with reservations.
55 Reserves are stored in reserves table.
56 The following columns contains important values :
57 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
58 =0 : then the reserve is being dealed
59 - found : NULL : means the patron requested the 1st available, and we haven't choosen the item
60 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
61 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
62 F(inished) : the reserve has been completed, and is done
63 - itemnumber : empty : the reserve is still unaffected to an item
64 filled: the reserve is attached to an item
65 The complete workflow is :
66 ==== 1st use case ====
67 patron request a document, 1st available : P >0, F=NULL, I=NULL
68 a library having it run "transfertodo", and clic on the list
69 if there is no transfer to do, the reserve waiting
70 patron can pick it up P =0, F=W, I=filled
71 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
72 The pickup library recieve the book, it check in P =0, F=W, I=filled
73 The patron borrow the book P =0, F=F, I=filled
75 ==== 2nd use case ====
76 patron requests a document, a given item,
77 If pickup is holding branch P =0, F=W, I=filled
78 If transfer needed, write in branchtransfer P =0, F=T, I=filled
79 The pickup library receive the book, it checks it in P =0, F=W, I=filled
80 The patron borrow the book P =0, F=F, I=filled
82 =head1 FUNCTIONS
84 =cut
86 BEGIN {
87 # set the version for version checking
88 $VERSION = 3.01;
89 require Exporter;
90 @ISA = qw(Exporter);
91 @EXPORT = qw(
92 &AddReserve
94 &GetReservesFromItemnumber
95 &GetReservesFromBiblionumber
96 &GetReservesFromBorrowernumber
97 &GetReservesForBranch
98 &GetReservesToBranch
99 &GetReserveCount
100 &GetReserveFee
101 &GetReserveInfo
102 &GetReserveStatus
104 &GetOtherReserves
106 &ModReserveFill
107 &ModReserveAffect
108 &ModReserve
109 &ModReserveStatus
110 &ModReserveCancelAll
111 &ModReserveMinusPriority
113 &CheckReserves
114 &CanBookBeReserved
115 &CanItemBeReserved
116 &CancelReserve
117 &CancelExpiredReserves
119 &IsAvailableForItemLevelRequest
121 &AlterPriority
122 &ToggleLowestPriority
126 =head2 AddReserve
128 AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
130 =cut
132 sub AddReserve {
133 my (
134 $branch, $borrowernumber, $biblionumber,
135 $constraint, $bibitems, $priority, $resdate, $expdate, $notes,
136 $title, $checkitem, $found
137 ) = @_;
138 my $fee =
139 GetReserveFee($borrowernumber, $biblionumber, $constraint,
140 $bibitems );
141 my $dbh = C4::Context->dbh;
142 my $const = lc substr( $constraint, 0, 1 );
143 $resdate = format_date_in_iso( $resdate ) if ( $resdate );
144 $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
145 if ($expdate) {
146 $expdate = format_date_in_iso( $expdate );
147 } else {
148 undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
150 if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
151 # Make room in reserves for this before those of a later reserve date
152 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
154 my $waitingdate;
156 # If the reserv had the waiting status, we had the value of the resdate
157 if ( $found eq 'W' ) {
158 $waitingdate = $resdate;
161 #eval {
162 # updates take place here
163 if ( $fee > 0 ) {
164 my $nextacctno = &getnextacctno( $borrowernumber );
165 my $query = qq/
166 INSERT INTO accountlines
167 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
168 VALUES
169 (?,?,now(),?,?,'Res',?)
171 my $usth = $dbh->prepare($query);
172 $usth->execute( $borrowernumber, $nextacctno, $fee,
173 "Reserve Charge - $title", $fee );
176 #if ($const eq 'a'){
177 my $query = qq/
178 INSERT INTO reserves
179 (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
180 priority,reservenotes,itemnumber,found,waitingdate,expirationdate)
181 VALUES
182 (?,?,?,?,?,
183 ?,?,?,?,?,?)
185 my $sth = $dbh->prepare($query);
186 $sth->execute(
187 $borrowernumber, $biblionumber, $resdate, $branch,
188 $const, $priority, $notes, $checkitem,
189 $found, $waitingdate, $expdate
192 # Send e-mail to librarian if syspref is active
193 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
194 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
195 my $biblio = GetBiblioData($biblionumber);
196 my $letter = C4::Letters::getletter( 'reserves', 'HOLDPLACED');
197 my $branchcode = $borrower->{branchcode};
198 my $branch_details = C4::Branch::GetBranchDetail($branchcode);
199 my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
201 my %keys = (%$borrower, %$biblio);
202 foreach my $key (keys %keys) {
203 my $replacefield = "<<$key>>";
204 $letter->{content} =~ s/$replacefield/$keys{$key}/g;
205 $letter->{title} =~ s/$replacefield/$keys{$key}/g;
208 C4::Letters::EnqueueLetter(
209 { letter => $letter,
210 borrowernumber => $borrowernumber,
211 message_transport_type => 'email',
212 from_address => $admin_email_address,
213 to_address => $admin_email_address,
222 ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value?
223 $query = qq/
224 INSERT INTO reserveconstraints
225 (borrowernumber,biblionumber,reservedate,biblioitemnumber)
226 VALUES
227 (?,?,?,?)
229 $sth = $dbh->prepare($query); # keep prepare outside the loop!
230 foreach (@$bibitems) {
231 $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
234 return; # FIXME: why not have a useful return value?
237 =head2 GetReservesFromBiblionumber
239 ($count, $title_reserves) = &GetReserves($biblionumber);
241 This function gets the list of reservations for one C<$biblionumber>, returning a count
242 of the reserves and an arrayref pointing to the reserves for C<$biblionumber>.
244 =cut
246 sub GetReservesFromBiblionumber {
247 my ($biblionumber) = shift or return (0, []);
248 my ($all_dates) = shift;
249 my $dbh = C4::Context->dbh;
251 # Find the desired items in the reserves
252 my $query = "
253 SELECT branchcode,
254 timestamp AS rtimestamp,
255 priority,
256 biblionumber,
257 borrowernumber,
258 reservedate,
259 constrainttype,
260 found,
261 itemnumber,
262 reservenotes,
263 expirationdate,
264 lowestPriority
265 FROM reserves
266 WHERE biblionumber = ? ";
267 unless ( $all_dates ) {
268 $query .= "AND reservedate <= CURRENT_DATE()";
270 $query .= "ORDER BY priority";
271 my $sth = $dbh->prepare($query);
272 $sth->execute($biblionumber);
273 my @results;
274 my $i = 0;
275 while ( my $data = $sth->fetchrow_hashref ) {
277 # FIXME - What is this doing? How do constraints work?
278 if ($data->{constrainttype} eq 'o') {
279 $query = '
280 SELECT biblioitemnumber
281 FROM reserveconstraints
282 WHERE biblionumber = ?
283 AND borrowernumber = ?
284 AND reservedate = ?
286 my $csth = $dbh->prepare($query);
287 $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
288 my @bibitemno;
289 while ( my $bibitemnos = $csth->fetchrow_array ) {
290 push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref
292 my $count = scalar @bibitemno;
294 # if we have two or more different specific itemtypes
295 # reserved by same person on same day
296 my $bdata;
297 if ( $count > 1 ) {
298 $bdata = GetBiblioItemData( $bibitemno[$i] ); # FIXME: This doesn't make sense.
299 $i++; # $i can increase each pass, but the next @bibitemno might be smaller?
301 else {
302 # Look up the book we just found.
303 $bdata = GetBiblioItemData( $bibitemno[0] );
305 # Add the results of this latest search to the current
306 # results.
307 # FIXME - An 'each' would probably be more efficient.
308 foreach my $key ( keys %$bdata ) {
309 $data->{$key} = $bdata->{$key};
312 push @results, $data;
314 return ( $#results + 1, \@results );
317 =head2 GetReservesFromItemnumber
319 ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber);
321 TODO :: Description here
323 =cut
325 sub GetReservesFromItemnumber {
326 my ( $itemnumber, $all_dates ) = @_;
327 my $dbh = C4::Context->dbh;
328 my $query = "
329 SELECT reservedate,borrowernumber,branchcode
330 FROM reserves
331 WHERE itemnumber=?
333 unless ( $all_dates ) {
334 $query .= " AND reservedate <= CURRENT_DATE()";
336 my $sth_res = $dbh->prepare($query);
337 $sth_res->execute($itemnumber);
338 my ( $reservedate, $borrowernumber,$branchcode ) = $sth_res->fetchrow_array;
339 return ( $reservedate, $borrowernumber, $branchcode );
342 =head2 GetReservesFromBorrowernumber
344 $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
346 TODO :: Descritpion
348 =cut
350 sub GetReservesFromBorrowernumber {
351 my ( $borrowernumber, $status ) = @_;
352 my $dbh = C4::Context->dbh;
353 my $sth;
354 if ($status) {
355 $sth = $dbh->prepare("
356 SELECT *
357 FROM reserves
358 WHERE borrowernumber=?
359 AND found =?
360 ORDER BY reservedate
362 $sth->execute($borrowernumber,$status);
363 } else {
364 $sth = $dbh->prepare("
365 SELECT *
366 FROM reserves
367 WHERE borrowernumber=?
368 ORDER BY reservedate
370 $sth->execute($borrowernumber);
372 my $data = $sth->fetchall_arrayref({});
373 return @$data;
375 #-------------------------------------------------------------------------------------
376 =head2 CanBookBeReserved
378 $error = &CanBookBeReserved($borrowernumber, $biblionumber)
380 =cut
382 sub CanBookBeReserved{
383 my ($borrowernumber, $biblionumber) = @_;
385 my @items = GetItemsInfo($biblionumber);
386 foreach my $item (@items){
387 return 1 if CanItemBeReserved($borrowernumber, $item->{itemnumber});
389 return 0;
392 =head2 CanItemBeReserved
394 $error = &CanItemBeReserved($borrowernumber, $itemnumber)
396 This function return 1 if an item can be issued by this borrower.
398 =cut
400 sub CanItemBeReserved{
401 my ($borrowernumber, $itemnumber) = @_;
403 my $dbh = C4::Context->dbh;
404 my $allowedreserves = 0;
406 my $controlbranch = C4::Context->preference('ReservesControlBranch');
407 my $itype = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype";
409 # we retrieve borrowers and items informations #
410 my $item = GetItem($itemnumber);
411 my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
413 # we retrieve user rights on this itemtype and branchcode
414 my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
415 FROM issuingrules
416 WHERE (categorycode in (?,'*') )
417 AND (itemtype IN (?,'*'))
418 AND (branchcode IN (?,'*'))
419 ORDER BY
420 categorycode DESC,
421 itemtype DESC,
422 branchcode DESC;"
425 my $querycount ="SELECT
426 count(*) as count
427 FROM reserves
428 LEFT JOIN items USING (itemnumber)
429 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
430 LEFT JOIN borrowers USING (borrowernumber)
431 WHERE borrowernumber = ?
435 my $itemtype = $item->{$itype};
436 my $categorycode = $borrower->{categorycode};
437 my $branchcode = "";
438 my $branchfield = "reserves.branchcode";
440 if( $controlbranch eq "ItemHomeLibrary" ){
441 $branchfield = "items.homebranch";
442 $branchcode = $item->{homebranch};
443 }elsif( $controlbranch eq "PatronLibrary" ){
444 $branchfield = "borrowers.branchcode";
445 $branchcode = $borrower->{branchcode};
448 # we retrieve rights
449 $sth->execute($categorycode, $itemtype, $branchcode);
450 if(my $rights = $sth->fetchrow_hashref()){
451 $itemtype = $rights->{itemtype};
452 $allowedreserves = $rights->{reservesallowed};
453 }else{
454 $itemtype = '*';
457 # we retrieve count
459 $querycount .= "AND $branchfield = ?";
461 $querycount .= " AND $itype = ?" if ($itemtype ne "*");
462 my $sthcount = $dbh->prepare($querycount);
464 if($itemtype eq "*"){
465 $sthcount->execute($borrowernumber, $branchcode);
466 }else{
467 $sthcount->execute($borrowernumber, $branchcode, $itemtype);
470 my $reservecount = "0";
471 if(my $rowcount = $sthcount->fetchrow_hashref()){
472 $reservecount = $rowcount->{count};
475 # we check if it's ok or not
476 if( $reservecount < $allowedreserves ){
477 return 1;
478 }else{
479 return 0;
482 #--------------------------------------------------------------------------------
483 =head2 GetReserveCount
485 $number = &GetReserveCount($borrowernumber);
487 this function returns the number of reservation for a borrower given on input arg.
489 =cut
491 sub GetReserveCount {
492 my ($borrowernumber) = @_;
494 my $dbh = C4::Context->dbh;
496 my $query = '
497 SELECT COUNT(*) AS counter
498 FROM reserves
499 WHERE borrowernumber = ?
501 my $sth = $dbh->prepare($query);
502 $sth->execute($borrowernumber);
503 my $row = $sth->fetchrow_hashref;
504 return $row->{counter};
507 =head2 GetOtherReserves
509 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
511 Check queued list of this document and check if this document must be transfered
513 =cut
515 sub GetOtherReserves {
516 my ($itemnumber) = @_;
517 my $messages;
518 my $nextreservinfo;
519 my ( $restype, $checkreserves ) = CheckReserves($itemnumber);
520 if ($checkreserves) {
521 my $iteminfo = GetItem($itemnumber);
522 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
523 $messages->{'transfert'} = $checkreserves->{'branchcode'};
524 #minus priorities of others reservs
525 ModReserveMinusPriority(
526 $itemnumber,
527 $checkreserves->{'borrowernumber'},
528 $iteminfo->{'biblionumber'}
531 #launch the subroutine dotransfer
532 C4::Items::ModItemTransfer(
533 $itemnumber,
534 $iteminfo->{'holdingbranch'},
535 $checkreserves->{'branchcode'}
540 #step 2b : case of a reservation on the same branch, set the waiting status
541 else {
542 $messages->{'waiting'} = 1;
543 ModReserveMinusPriority(
544 $itemnumber,
545 $checkreserves->{'borrowernumber'},
546 $iteminfo->{'biblionumber'}
548 ModReserveStatus($itemnumber,'W');
551 $nextreservinfo = $checkreserves->{'borrowernumber'};
554 return ( $messages, $nextreservinfo );
557 =head2 GetReserveFee
559 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
561 Calculate the fee for a reserve
563 =cut
565 sub GetReserveFee {
566 my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
568 #check for issues;
569 my $dbh = C4::Context->dbh;
570 my $const = lc substr( $constraint, 0, 1 );
571 my $query = qq/
572 SELECT * FROM borrowers
573 LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
574 WHERE borrowernumber = ?
576 my $sth = $dbh->prepare($query);
577 $sth->execute($borrowernumber);
578 my $data = $sth->fetchrow_hashref;
579 $sth->finish();
580 my $fee = $data->{'reservefee'};
581 my $cntitems = @- > $bibitems;
583 if ( $fee > 0 ) {
585 # check for items on issue
586 # first find biblioitem records
587 my @biblioitems;
588 my $sth1 = $dbh->prepare(
589 "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
590 WHERE (biblio.biblionumber = ?)"
592 $sth1->execute($biblionumber);
593 while ( my $data1 = $sth1->fetchrow_hashref ) {
594 if ( $const eq "a" ) {
595 push @biblioitems, $data1;
597 else {
598 my $found = 0;
599 my $x = 0;
600 while ( $x < $cntitems ) {
601 if ( @$bibitems->{'biblioitemnumber'} ==
602 $data->{'biblioitemnumber'} )
604 $found = 1;
606 $x++;
608 if ( $const eq 'o' ) {
609 if ( $found == 1 ) {
610 push @biblioitems, $data1;
613 else {
614 if ( $found == 0 ) {
615 push @biblioitems, $data1;
620 $sth1->finish;
621 my $cntitemsfound = @biblioitems;
622 my $issues = 0;
623 my $x = 0;
624 my $allissued = 1;
625 while ( $x < $cntitemsfound ) {
626 my $bitdata = $biblioitems[$x];
627 my $sth2 = $dbh->prepare(
628 "SELECT * FROM items
629 WHERE biblioitemnumber = ?"
631 $sth2->execute( $bitdata->{'biblioitemnumber'} );
632 while ( my $itdata = $sth2->fetchrow_hashref ) {
633 my $sth3 = $dbh->prepare(
634 "SELECT * FROM issues
635 WHERE itemnumber = ?"
637 $sth3->execute( $itdata->{'itemnumber'} );
638 if ( my $isdata = $sth3->fetchrow_hashref ) {
640 else {
641 $allissued = 0;
644 $x++;
646 if ( $allissued == 0 ) {
647 my $rsth =
648 $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
649 $rsth->execute($biblionumber);
650 if ( my $rdata = $rsth->fetchrow_hashref ) {
652 else {
653 $fee = 0;
657 return $fee;
660 =head2 GetReservesToBranch
662 @transreserv = GetReservesToBranch( $frombranch );
664 Get reserve list for a given branch
666 =cut
668 sub GetReservesToBranch {
669 my ( $frombranch ) = @_;
670 my $dbh = C4::Context->dbh;
671 my $sth = $dbh->prepare(
672 "SELECT borrowernumber,reservedate,itemnumber,timestamp
673 FROM reserves
674 WHERE priority='0'
675 AND branchcode=?"
677 $sth->execute( $frombranch );
678 my @transreserv;
679 my $i = 0;
680 while ( my $data = $sth->fetchrow_hashref ) {
681 $transreserv[$i] = $data;
682 $i++;
684 return (@transreserv);
687 =head2 GetReservesForBranch
689 @transreserv = GetReservesForBranch($frombranch);
691 =cut
693 sub GetReservesForBranch {
694 my ($frombranch) = @_;
695 my $dbh = C4::Context->dbh;
696 my $query = "SELECT borrowernumber,reservedate,itemnumber,waitingdate
697 FROM reserves
698 WHERE priority='0'
699 AND found='W' ";
700 if ($frombranch){
701 $query .= " AND branchcode=? ";
703 $query .= "ORDER BY waitingdate" ;
704 my $sth = $dbh->prepare($query);
705 if ($frombranch){
706 $sth->execute($frombranch);
708 else {
709 $sth->execute();
711 my @transreserv;
712 my $i = 0;
713 while ( my $data = $sth->fetchrow_hashref ) {
714 $transreserv[$i] = $data;
715 $i++;
717 return (@transreserv);
720 sub GetReserveStatus {
721 my ($itemnumber) = @_;
723 my $dbh = C4::Context->dbh;
725 my $itemstatus = $dbh->prepare("SELECT found FROM reserves WHERE itemnumber = ?");
727 $itemstatus->execute($itemnumber);
728 my ($found) = $itemstatus->fetchrow_array;
729 return $found;
732 =head2 CheckReserves
734 ($status, $reserve) = &CheckReserves($itemnumber);
735 ($status, $reserve) = &CheckReserves(undef, $barcode);
737 Find a book in the reserves.
739 C<$itemnumber> is the book's item number.
741 As I understand it, C<&CheckReserves> looks for the given item in the
742 reserves. If it is found, that's a match, and C<$status> is set to
743 C<Waiting>.
745 Otherwise, it finds the most important item in the reserves with the
746 same biblio number as this book (I'm not clear on this) and returns it
747 with C<$status> set to C<Reserved>.
749 C<&CheckReserves> returns a two-element list:
751 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
753 C<$reserve> is the reserve item that matched. It is a
754 reference-to-hash whose keys are mostly the fields of the reserves
755 table in the Koha database.
757 =cut
759 sub CheckReserves {
760 my ( $item, $barcode ) = @_;
761 my $dbh = C4::Context->dbh;
762 my $sth;
763 my $select = "
764 SELECT items.biblionumber,
765 items.biblioitemnumber,
766 itemtypes.notforloan,
767 items.notforloan AS itemnotforloan,
768 items.itemnumber
769 FROM items
770 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
771 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
774 if ($item) {
775 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
776 $sth->execute($item);
778 else {
779 $sth = $dbh->prepare("$select WHERE barcode = ?");
780 $sth->execute($barcode);
782 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
783 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
785 return ( 0, 0 ) unless $itemnumber; # bail if we got nothing.
787 # if item is not for loan it cannot be reserved either.....
788 # execpt where items.notforloan < 0 : This indicates the item is holdable.
789 return ( 0, 0 ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
791 # Find this item in the reserves
792 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber );
794 # $priority and $highest are used to find the most important item
795 # in the list returned by &_Findgroupreserve. (The lower $priority,
796 # the more important the item.)
797 # $highest is the most important item we've seen so far.
798 my $highest;
799 if (scalar @reserves) {
800 my $priority = 10000000;
801 foreach my $res (@reserves) {
802 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
803 return ( "Waiting", $res ); # Found it
804 } else {
805 # See if this item is more important than what we've got so far
806 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
807 $priority = $res->{'priority'};
808 $highest = $res;
814 # If we get this far, then no exact match was found.
815 # We return the most important (i.e. next) reservation.
816 if ($highest) {
817 $highest->{'itemnumber'} = $item;
818 return ( "Reserved", $highest );
820 else {
821 return ( 0, 0 );
825 =head2 CancelExpiredReserves
827 CancelExpiredReserves();
829 Cancels all reserves with an expiration date from before today.
831 =cut
833 sub CancelExpiredReserves {
835 my $dbh = C4::Context->dbh;
836 my $sth = $dbh->prepare( "
837 SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
838 AND expirationdate IS NOT NULL
839 " );
840 $sth->execute();
842 while ( my $res = $sth->fetchrow_hashref() ) {
843 CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
848 =head2 CancelReserve
850 &CancelReserve($biblionumber, $itemnumber, $borrowernumber);
852 Cancels a reserve.
854 Use either C<$biblionumber> or C<$itemnumber> to specify the item to
855 cancel, but not both: if both are given, C<&CancelReserve> does
856 nothing.
858 C<$borrowernumber> is the borrower number of the patron on whose
859 behalf the book was reserved.
861 If C<$biblionumber> was given, C<&CancelReserve> also adjusts the
862 priorities of the other people who are waiting on the book.
864 =cut
866 sub CancelReserve {
867 my ( $biblio, $item, $borr ) = @_;
868 my $dbh = C4::Context->dbh;
869 if ( $item and $borr ) {
870 # removing a waiting reserve record....
871 # update the database...
872 my $query = "
873 UPDATE reserves
874 SET cancellationdate = now(),
875 found = Null,
876 priority = 0
877 WHERE itemnumber = ?
878 AND borrowernumber = ?
880 my $sth = $dbh->prepare($query);
881 $sth->execute( $item, $borr );
882 $sth->finish;
883 $query = "
884 INSERT INTO old_reserves
885 SELECT * FROM reserves
886 WHERE itemnumber = ?
887 AND borrowernumber = ?
889 $sth = $dbh->prepare($query);
890 $sth->execute( $item, $borr );
891 $query = "
892 DELETE FROM reserves
893 WHERE itemnumber = ?
894 AND borrowernumber = ?
896 $sth = $dbh->prepare($query);
897 $sth->execute( $item, $borr );
899 else {
900 # removing a reserve record....
901 # get the prioritiy on this record....
902 my $priority;
903 my $query = qq/
904 SELECT priority FROM reserves
905 WHERE biblionumber = ?
906 AND borrowernumber = ?
907 AND cancellationdate IS NULL
908 AND itemnumber IS NULL
910 my $sth = $dbh->prepare($query);
911 $sth->execute( $biblio, $borr );
912 ($priority) = $sth->fetchrow_array;
913 $sth->finish;
914 $query = qq/
915 UPDATE reserves
916 SET cancellationdate = now(),
917 found = Null,
918 priority = 0
919 WHERE biblionumber = ?
920 AND borrowernumber = ?
923 # update the database, removing the record...
924 $sth = $dbh->prepare($query);
925 $sth->execute( $biblio, $borr );
926 $sth->finish;
928 $query = qq/
929 INSERT INTO old_reserves
930 SELECT * FROM reserves
931 WHERE biblionumber = ?
932 AND borrowernumber = ?
934 $sth = $dbh->prepare($query);
935 $sth->execute( $biblio, $borr );
937 $query = qq/
938 DELETE FROM reserves
939 WHERE biblionumber = ?
940 AND borrowernumber = ?
942 $sth = $dbh->prepare($query);
943 $sth->execute( $biblio, $borr );
945 # now fix the priority on the others....
946 _FixPriority( $biblio, $borr );
950 =head2 ModReserve
952 ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber])
954 Change a hold request's priority or cancel it.
956 C<$rank> specifies the effect of the change. If C<$rank>
957 is 'W' or 'n', nothing happens. This corresponds to leaving a
958 request alone when changing its priority in the holds queue
959 for a bib.
961 If C<$rank> is 'del', the hold request is cancelled.
963 If C<$rank> is an integer greater than zero, the priority of
964 the request is set to that value. Since priority != 0 means
965 that the item is not waiting on the hold shelf, setting the
966 priority to a non-zero value also sets the request's found
967 status and waiting date to NULL.
969 The optional C<$itemnumber> parameter is used only when
970 C<$rank> is a non-zero integer; if supplied, the itemnumber
971 of the hold request is set accordingly; if omitted, the itemnumber
972 is cleared.
974 B<FIXME:> Note that the forgoing can have the effect of causing
975 item-level hold requests to turn into title-level requests. This
976 will be fixed once reserves has separate columns for requested
977 itemnumber and supplying itemnumber.
979 =cut
981 sub ModReserve {
982 #subroutine to update a reserve
983 my ( $rank, $biblio, $borrower, $branch , $itemnumber) = @_;
984 return if $rank eq "W";
985 return if $rank eq "n";
986 my $dbh = C4::Context->dbh;
987 if ( $rank eq "del" ) {
988 my $query = qq/
989 UPDATE reserves
990 SET cancellationdate=now()
991 WHERE biblionumber = ?
992 AND borrowernumber = ?
994 my $sth = $dbh->prepare($query);
995 $sth->execute( $biblio, $borrower );
996 $sth->finish;
997 $query = qq/
998 INSERT INTO old_reserves
999 SELECT *
1000 FROM reserves
1001 WHERE biblionumber = ?
1002 AND borrowernumber = ?
1004 $sth = $dbh->prepare($query);
1005 $sth->execute( $biblio, $borrower );
1006 $query = qq/
1007 DELETE FROM reserves
1008 WHERE biblionumber = ?
1009 AND borrowernumber = ?
1011 $sth = $dbh->prepare($query);
1012 $sth->execute( $biblio, $borrower );
1015 elsif ($rank =~ /^\d+/ and $rank > 0) {
1016 my $query = qq/
1017 UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1018 WHERE biblionumber = ?
1019 AND borrowernumber = ?
1021 my $sth = $dbh->prepare($query);
1022 $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower);
1023 $sth->finish;
1024 _FixPriority( $biblio, $borrower, $rank);
1028 =head2 ModReserveFill
1030 &ModReserveFill($reserve);
1032 Fill a reserve. If I understand this correctly, this means that the
1033 reserved book has been found and given to the patron who reserved it.
1035 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1036 whose keys are fields from the reserves table in the Koha database.
1038 =cut
1040 sub ModReserveFill {
1041 my ($res) = @_;
1042 my $dbh = C4::Context->dbh;
1043 # fill in a reserve record....
1044 my $biblionumber = $res->{'biblionumber'};
1045 my $borrowernumber = $res->{'borrowernumber'};
1046 my $resdate = $res->{'reservedate'};
1048 # get the priority on this record....
1049 my $priority;
1050 my $query = "SELECT priority
1051 FROM reserves
1052 WHERE biblionumber = ?
1053 AND borrowernumber = ?
1054 AND reservedate = ?";
1055 my $sth = $dbh->prepare($query);
1056 $sth->execute( $biblionumber, $borrowernumber, $resdate );
1057 ($priority) = $sth->fetchrow_array;
1058 $sth->finish;
1060 # update the database...
1061 $query = "UPDATE reserves
1062 SET found = 'F',
1063 priority = 0
1064 WHERE biblionumber = ?
1065 AND reservedate = ?
1066 AND borrowernumber = ?
1068 $sth = $dbh->prepare($query);
1069 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1070 $sth->finish;
1072 # move to old_reserves
1073 $query = "INSERT INTO old_reserves
1074 SELECT * FROM reserves
1075 WHERE biblionumber = ?
1076 AND reservedate = ?
1077 AND borrowernumber = ?
1079 $sth = $dbh->prepare($query);
1080 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1081 $query = "DELETE FROM reserves
1082 WHERE biblionumber = ?
1083 AND reservedate = ?
1084 AND borrowernumber = ?
1086 $sth = $dbh->prepare($query);
1087 $sth->execute( $biblionumber, $resdate, $borrowernumber );
1089 # now fix the priority on the others (if the priority wasn't
1090 # already sorted!)....
1091 unless ( $priority == 0 ) {
1092 _FixPriority( $biblionumber, $borrowernumber );
1096 =head2 ModReserveStatus
1098 &ModReserveStatus($itemnumber, $newstatus);
1100 Update the reserve status for the active (priority=0) reserve.
1102 $itemnumber is the itemnumber the reserve is on
1104 $newstatus is the new status.
1106 =cut
1108 sub ModReserveStatus {
1110 #first : check if we have a reservation for this item .
1111 my ($itemnumber, $newstatus) = @_;
1112 my $dbh = C4::Context->dbh;
1113 my $query = " UPDATE reserves
1114 SET found=?,waitingdate = now()
1115 WHERE itemnumber=?
1116 AND found IS NULL
1117 AND priority = 0
1119 my $sth_set = $dbh->prepare($query);
1120 $sth_set->execute( $newstatus, $itemnumber );
1122 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1123 CartToShelf( $itemnumber );
1127 =head2 ModReserveAffect
1129 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1131 This function affect an item and a status for a given reserve
1132 The itemnumber parameter is used to find the biblionumber.
1133 with the biblionumber & the borrowernumber, we can affect the itemnumber
1134 to the correct reserve.
1136 if $transferToDo is not set, then the status is set to "Waiting" as well.
1137 otherwise, a transfer is on the way, and the end of the transfer will
1138 take care of the waiting status
1140 =cut
1142 sub ModReserveAffect {
1143 my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1144 my $dbh = C4::Context->dbh;
1146 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1147 # attached to $itemnumber
1148 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1149 $sth->execute($itemnumber);
1150 my ($biblionumber) = $sth->fetchrow;
1152 # get request - need to find out if item is already
1153 # waiting in order to not send duplicate hold filled notifications
1154 my $request = GetReserveInfo($borrowernumber, $biblionumber);
1155 my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1157 # If we affect a reserve that has to be transfered, don't set to Waiting
1158 my $query;
1159 if ($transferToDo) {
1160 $query = "
1161 UPDATE reserves
1162 SET priority = 0,
1163 itemnumber = ?,
1164 found = 'T'
1165 WHERE borrowernumber = ?
1166 AND biblionumber = ?
1169 else {
1170 # affect the reserve to Waiting as well.
1171 $query = "
1172 UPDATE reserves
1173 SET priority = 0,
1174 found = 'W',
1175 waitingdate=now(),
1176 itemnumber = ?
1177 WHERE borrowernumber = ?
1178 AND biblionumber = ?
1181 $sth = $dbh->prepare($query);
1182 $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1183 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1185 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1186 CartToShelf( $itemnumber );
1189 return;
1192 =head2 ModReserveCancelAll
1194 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1196 function to cancel reserv,check other reserves, and transfer document if it's necessary
1198 =cut
1200 sub ModReserveCancelAll {
1201 my $messages;
1202 my $nextreservinfo;
1203 my ( $itemnumber, $borrowernumber ) = @_;
1205 #step 1 : cancel the reservation
1206 my $CancelReserve = CancelReserve( undef, $itemnumber, $borrowernumber );
1208 #step 2 launch the subroutine of the others reserves
1209 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1211 return ( $messages, $nextreservinfo );
1214 =head2 ModReserveMinusPriority
1216 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1218 Reduce the values of queuded list
1220 =cut
1222 sub ModReserveMinusPriority {
1223 my ( $itemnumber, $borrowernumber, $biblionumber ) = @_;
1225 #first step update the value of the first person on reserv
1226 my $dbh = C4::Context->dbh;
1227 my $query = "
1228 UPDATE reserves
1229 SET priority = 0 , itemnumber = ?
1230 WHERE borrowernumber=?
1231 AND biblionumber=?
1233 my $sth_upd = $dbh->prepare($query);
1234 $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber );
1235 # second step update all others reservs
1236 _FixPriority($biblionumber, $borrowernumber, '0');
1239 =head2 GetReserveInfo
1241 &GetReserveInfo($borrowernumber,$biblionumber);
1243 Get item and borrower details for a current hold.
1244 Current implementation this query should have a single result.
1246 =cut
1248 sub GetReserveInfo {
1249 my ( $borrowernumber, $biblionumber ) = @_;
1250 my $dbh = C4::Context->dbh;
1251 my $strsth="SELECT
1252 reservedate,
1253 reservenotes,
1254 reserves.borrowernumber,
1255 reserves.biblionumber,
1256 reserves.branchcode,
1257 reserves.waitingdate,
1258 notificationdate,
1259 reminderdate,
1260 priority,
1261 found,
1262 firstname,
1263 surname,
1264 phone,
1265 email,
1266 address,
1267 address2,
1268 cardnumber,
1269 city,
1270 zipcode,
1271 biblio.title,
1272 biblio.author,
1273 items.holdingbranch,
1274 items.itemcallnumber,
1275 items.itemnumber,
1276 items.location,
1277 barcode,
1278 notes
1279 FROM reserves
1280 LEFT JOIN items USING(itemnumber)
1281 LEFT JOIN borrowers USING(borrowernumber)
1282 LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber)
1283 WHERE
1284 reserves.borrowernumber=?
1285 AND reserves.biblionumber=?";
1286 my $sth = $dbh->prepare($strsth);
1287 $sth->execute($borrowernumber,$biblionumber);
1289 my $data = $sth->fetchrow_hashref;
1290 return $data;
1294 =head2 IsAvailableForItemLevelRequest
1296 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1298 Checks whether a given item record is available for an
1299 item-level hold request. An item is available if
1301 * it is not lost AND
1302 * it is not damaged AND
1303 * it is not withdrawn AND
1304 * does not have a not for loan value > 0
1306 Whether or not the item is currently on loan is
1307 also checked - if the AllowOnShelfHolds system preference
1308 is ON, an item can be requested even if it is currently
1309 on loan to somebody else. If the system preference
1310 is OFF, an item that is currently checked out cannot
1311 be the target of an item-level hold request.
1313 Note that IsAvailableForItemLevelRequest() does not
1314 check if the staff operator is authorized to place
1315 a request on the item - in particular,
1316 this routine does not check IndependantBranches
1317 and canreservefromotherbranches.
1319 =cut
1321 sub IsAvailableForItemLevelRequest {
1322 my $itemnumber = shift;
1324 my $item = GetItem($itemnumber);
1326 # must check the notforloan setting of the itemtype
1327 # FIXME - a lot of places in the code do this
1328 # or something similar - need to be
1329 # consolidated
1330 my $dbh = C4::Context->dbh;
1331 my $notforloan_query;
1332 if (C4::Context->preference('item-level_itypes')) {
1333 $notforloan_query = "SELECT itemtypes.notforloan
1334 FROM items
1335 JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1336 WHERE itemnumber = ?";
1337 } else {
1338 $notforloan_query = "SELECT itemtypes.notforloan
1339 FROM items
1340 JOIN biblioitems USING (biblioitemnumber)
1341 JOIN itemtypes USING (itemtype)
1342 WHERE itemnumber = ?";
1344 my $sth = $dbh->prepare($notforloan_query);
1345 $sth->execute($itemnumber);
1346 my $notforloan_per_itemtype = 0;
1347 if (my ($notforloan) = $sth->fetchrow_array) {
1348 $notforloan_per_itemtype = 1 if $notforloan;
1351 my $available_per_item = 1;
1352 $available_per_item = 0 if $item->{itemlost} or
1353 ( $item->{notforloan} > 0 ) or
1354 ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1355 $item->{wthdrawn} or
1356 $notforloan_per_itemtype;
1359 if (C4::Context->preference('AllowOnShelfHolds')) {
1360 return $available_per_item;
1361 } else {
1362 return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "W"));
1366 =head2 AlterPriority
1368 AlterPriority( $where, $borrowernumber, $biblionumber, $reservedate );
1370 This function changes a reserve's priority up, down, to the top, or to the bottom.
1371 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1373 =cut
1375 sub AlterPriority {
1376 my ( $where, $borrowernumber, $biblionumber ) = @_;
1378 my $dbh = C4::Context->dbh;
1380 ## Find this reserve
1381 my $sth = $dbh->prepare('SELECT * FROM reserves WHERE biblionumber = ? AND borrowernumber = ? AND cancellationdate IS NULL');
1382 $sth->execute( $biblionumber, $borrowernumber );
1383 my $reserve = $sth->fetchrow_hashref();
1384 $sth->finish();
1386 if ( $where eq 'up' || $where eq 'down' ) {
1388 my $priority = $reserve->{'priority'};
1389 $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1390 _FixPriority( $biblionumber, $borrowernumber, $priority )
1392 } elsif ( $where eq 'top' ) {
1394 _FixPriority( $biblionumber, $borrowernumber, '1' )
1396 } elsif ( $where eq 'bottom' ) {
1398 _FixPriority( $biblionumber, $borrowernumber, '999999' )
1403 =head2 ToggleLowestPriority
1405 ToggleLowestPriority( $borrowernumber, $biblionumber );
1407 This function sets the lowestPriority field to true if is false, and false if it is true.
1409 =cut
1411 sub ToggleLowestPriority {
1412 my ( $borrowernumber, $biblionumber ) = @_;
1414 my $dbh = C4::Context->dbh;
1416 my $sth = $dbh->prepare(
1417 "UPDATE reserves SET lowestPriority = NOT lowestPriority
1418 WHERE biblionumber = ?
1419 AND borrowernumber = ?"
1421 $sth->execute(
1422 $biblionumber,
1423 $borrowernumber,
1425 $sth->finish;
1427 _FixPriority( $biblionumber, $borrowernumber, '999999' );
1430 =head2 _FixPriority
1432 &_FixPriority($biblio,$borrowernumber,$rank,$ignoreSetLowestRank);
1434 Only used internally (so don't export it)
1435 Changed how this functions works #
1436 Now just gets an array of reserves in the rank order and updates them with
1437 the array index (+1 as array starts from 0)
1438 and if $rank is supplied will splice item from the array and splice it back in again
1439 in new priority rank
1441 =cut
1443 sub _FixPriority {
1444 my ( $biblio, $borrowernumber, $rank, $ignoreSetLowestRank ) = @_;
1445 my $dbh = C4::Context->dbh;
1446 if ( $rank eq "del" ) {
1447 CancelReserve( $biblio, undef, $borrowernumber );
1449 if ( $rank eq "W" || $rank eq "0" ) {
1451 # make sure priority for waiting or in-transit items is 0
1452 my $query = qq/
1453 UPDATE reserves
1454 SET priority = 0
1455 WHERE biblionumber = ?
1456 AND borrowernumber = ?
1457 AND found IN ('W', 'T')
1459 my $sth = $dbh->prepare($query);
1460 $sth->execute( $biblio, $borrowernumber );
1462 my @priority;
1463 my @reservedates;
1465 # get whats left
1466 # FIXME adding a new security in returned elements for changing priority,
1467 # now, we don't care anymore any reservations with itemnumber linked (suppose a waiting reserve)
1468 # This is wrong a waiting reserve has W set
1469 # The assumption that having an itemnumber set means waiting is wrong and should be corrected any place it occurs
1470 my $query = qq/
1471 SELECT borrowernumber, reservedate, constrainttype
1472 FROM reserves
1473 WHERE biblionumber = ?
1474 AND ((found <> 'W' AND found <> 'T') or found is NULL)
1475 ORDER BY priority ASC
1477 my $sth = $dbh->prepare($query);
1478 $sth->execute($biblio);
1479 while ( my $line = $sth->fetchrow_hashref ) {
1480 push( @reservedates, $line );
1481 push( @priority, $line );
1484 # To find the matching index
1485 my $i;
1486 my $key = -1; # to allow for 0 to be a valid result
1487 for ( $i = 0 ; $i < @priority ; $i++ ) {
1488 if ( $borrowernumber == $priority[$i]->{'borrowernumber'} ) {
1489 $key = $i; # save the index
1490 last;
1494 # if index exists in array then move it to new position
1495 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1496 my $new_rank = $rank -
1497 1; # $new_rank is what you want the new index to be in the array
1498 my $moving_item = splice( @priority, $key, 1 );
1499 splice( @priority, $new_rank, 0, $moving_item );
1502 # now fix the priority on those that are left....
1503 $query = "
1504 UPDATE reserves
1505 SET priority = ?
1506 WHERE biblionumber = ?
1507 AND borrowernumber = ?
1508 AND reservedate = ?
1509 AND found IS NULL
1511 $sth = $dbh->prepare($query);
1512 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1513 $sth->execute(
1514 $j + 1, $biblio,
1515 $priority[$j]->{'borrowernumber'},
1516 $priority[$j]->{'reservedate'}
1518 $sth->finish;
1521 $sth = $dbh->prepare( "SELECT borrowernumber FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1522 $sth->execute();
1524 unless ( $ignoreSetLowestRank ) {
1525 while ( my $res = $sth->fetchrow_hashref() ) {
1526 _FixPriority( $biblio, $res->{'borrowernumber'}, '999999', 1 );
1531 =head2 _Findgroupreserve
1533 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1535 Looks for an item-specific match first, then for a title-level match, returning the
1536 first match found. If neither, then we look for a 3rd kind of match based on
1537 reserve constraints.
1539 TODO: add more explanation about reserve constraints
1541 C<&_Findgroupreserve> returns :
1542 C<@results> is an array of references-to-hash whose keys are mostly
1543 fields from the reserves table of the Koha database, plus
1544 C<biblioitemnumber>.
1546 =cut
1548 sub _Findgroupreserve {
1549 my ( $bibitem, $biblio, $itemnumber ) = @_;
1550 my $dbh = C4::Context->dbh;
1552 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1553 # check for exact targetted match
1554 my $item_level_target_query = qq/
1555 SELECT reserves.biblionumber AS biblionumber,
1556 reserves.borrowernumber AS borrowernumber,
1557 reserves.reservedate AS reservedate,
1558 reserves.branchcode AS branchcode,
1559 reserves.cancellationdate AS cancellationdate,
1560 reserves.found AS found,
1561 reserves.reservenotes AS reservenotes,
1562 reserves.priority AS priority,
1563 reserves.timestamp AS timestamp,
1564 biblioitems.biblioitemnumber AS biblioitemnumber,
1565 reserves.itemnumber AS itemnumber
1566 FROM reserves
1567 JOIN biblioitems USING (biblionumber)
1568 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1569 WHERE found IS NULL
1570 AND priority > 0
1571 AND item_level_request = 1
1572 AND itemnumber = ?
1573 AND reservedate <= CURRENT_DATE()
1575 my $sth = $dbh->prepare($item_level_target_query);
1576 $sth->execute($itemnumber);
1577 my @results;
1578 if ( my $data = $sth->fetchrow_hashref ) {
1579 push( @results, $data );
1581 return @results if @results;
1583 # check for title-level targetted match
1584 my $title_level_target_query = qq/
1585 SELECT reserves.biblionumber AS biblionumber,
1586 reserves.borrowernumber AS borrowernumber,
1587 reserves.reservedate AS reservedate,
1588 reserves.branchcode AS branchcode,
1589 reserves.cancellationdate AS cancellationdate,
1590 reserves.found AS found,
1591 reserves.reservenotes AS reservenotes,
1592 reserves.priority AS priority,
1593 reserves.timestamp AS timestamp,
1594 biblioitems.biblioitemnumber AS biblioitemnumber,
1595 reserves.itemnumber AS itemnumber
1596 FROM reserves
1597 JOIN biblioitems USING (biblionumber)
1598 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1599 WHERE found IS NULL
1600 AND priority > 0
1601 AND item_level_request = 0
1602 AND hold_fill_targets.itemnumber = ?
1603 AND reservedate <= CURRENT_DATE()
1605 $sth = $dbh->prepare($title_level_target_query);
1606 $sth->execute($itemnumber);
1607 @results = ();
1608 if ( my $data = $sth->fetchrow_hashref ) {
1609 push( @results, $data );
1611 return @results if @results;
1613 my $query = qq/
1614 SELECT reserves.biblionumber AS biblionumber,
1615 reserves.borrowernumber AS borrowernumber,
1616 reserves.reservedate AS reservedate,
1617 reserves.waitingdate AS waitingdate,
1618 reserves.branchcode AS branchcode,
1619 reserves.cancellationdate AS cancellationdate,
1620 reserves.found AS found,
1621 reserves.reservenotes AS reservenotes,
1622 reserves.priority AS priority,
1623 reserves.timestamp AS timestamp,
1624 reserveconstraints.biblioitemnumber AS biblioitemnumber,
1625 reserves.itemnumber AS itemnumber
1626 FROM reserves
1627 LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1628 WHERE reserves.biblionumber = ?
1629 AND ( ( reserveconstraints.biblioitemnumber = ?
1630 AND reserves.borrowernumber = reserveconstraints.borrowernumber
1631 AND reserves.reservedate = reserveconstraints.reservedate )
1632 OR reserves.constrainttype='a' )
1633 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1634 AND reserves.reservedate <= CURRENT_DATE()
1636 $sth = $dbh->prepare($query);
1637 $sth->execute( $biblio, $bibitem, $itemnumber );
1638 @results = ();
1639 while ( my $data = $sth->fetchrow_hashref ) {
1640 push( @results, $data );
1642 return @results;
1645 =head2 _koha_notify_reserve
1647 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1649 Sends a notification to the patron that their hold has been filled (through
1650 ModReserveAffect, _not_ ModReserveFill)
1652 =cut
1654 sub _koha_notify_reserve {
1655 my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1657 my $dbh = C4::Context->dbh;
1658 my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1659 my $letter_code;
1660 my $print_mode = 0;
1661 my $messagingprefs;
1662 if ( $borrower->{'email'} || $borrower->{'smsalertnumber'} ) {
1663 $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold Filled' } );
1665 return if ( !defined( $messagingprefs->{'letter_code'} ) );
1666 $letter_code = $messagingprefs->{'letter_code'};
1667 } else {
1668 $letter_code = 'HOLD_PRINT';
1669 $print_mode = 1;
1672 my $sth = $dbh->prepare("
1673 SELECT *
1674 FROM reserves
1675 WHERE borrowernumber = ?
1676 AND biblionumber = ?
1678 $sth->execute( $borrowernumber, $biblionumber );
1679 my $reserve = $sth->fetchrow_hashref;
1680 my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1682 my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1684 my $letter = getletter( 'reserves', $letter_code );
1685 die "Could not find a letter called '$letter_code' in the 'reserves' module" unless( $letter );
1687 C4::Letters::parseletter( $letter, 'branches', $reserve->{'branchcode'} );
1688 C4::Letters::parseletter( $letter, 'borrowers', $borrowernumber );
1689 C4::Letters::parseletter( $letter, 'biblio', $biblionumber );
1690 C4::Letters::parseletter( $letter, 'reserves', $borrowernumber, $biblionumber );
1692 if ( $reserve->{'itemnumber'} ) {
1693 C4::Letters::parseletter( $letter, 'items', $reserve->{'itemnumber'} );
1695 my $today = C4::Dates->new()->output();
1696 $letter->{'title'} =~ s/<<today>>/$today/g;
1697 $letter->{'content'} =~ s/<<today>>/$today/g;
1698 $letter->{'content'} =~ s/<<[a-z0-9_]+\.[a-z0-9]+>>//g; #remove any stragglers
1700 if ( $print_mode ) {
1701 C4::Letters::EnqueueLetter( {
1702 letter => $letter,
1703 borrowernumber => $borrowernumber,
1704 message_transport_type => 'print',
1705 } );
1707 return;
1710 if ( grep { $_ eq 'email' } @{$messagingprefs->{transports}} ) {
1711 # aka, 'email' in ->{'transports'}
1712 C4::Letters::EnqueueLetter(
1713 { letter => $letter,
1714 borrowernumber => $borrowernumber,
1715 message_transport_type => 'email',
1716 from_address => $admin_email_address,
1721 if ( grep { $_ eq 'sms' } @{$messagingprefs->{transports}} ) {
1722 C4::Letters::EnqueueLetter(
1723 { letter => $letter,
1724 borrowernumber => $borrowernumber,
1725 message_transport_type => 'sms',
1731 =head2 _ShiftPriorityByDateAndPriority
1733 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1735 This increments the priority of all reserves after the one
1736 with either the lowest date after C<$reservedate>
1737 or the lowest priority after C<$priority>.
1739 It effectively makes room for a new reserve to be inserted with a certain
1740 priority, which is returned.
1742 This is most useful when the reservedate can be set by the user. It allows
1743 the new reserve to be placed before other reserves that have a later
1744 reservedate. Since priority also is set by the form in reserves/request.pl
1745 the sub accounts for that too.
1747 =cut
1749 sub _ShiftPriorityByDateAndPriority {
1750 my ( $biblio, $resdate, $new_priority ) = @_;
1752 my $dbh = C4::Context->dbh;
1753 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1754 my $sth = $dbh->prepare( $query );
1755 $sth->execute( $biblio, $resdate, $new_priority );
1756 my $min_priority = $sth->fetchrow;
1757 # if no such matches are found, $new_priority remains as original value
1758 $new_priority = $min_priority if ( $min_priority );
1760 # Shift the priority up by one; works in conjunction with the next SQL statement
1761 $query = "UPDATE reserves
1762 SET priority = priority+1
1763 WHERE biblionumber = ?
1764 AND borrowernumber = ?
1765 AND reservedate = ?
1766 AND found IS NULL";
1767 my $sth_update = $dbh->prepare( $query );
1769 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1770 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1771 $sth = $dbh->prepare( $query );
1772 $sth->execute( $new_priority, $biblio );
1773 while ( my $row = $sth->fetchrow_hashref ) {
1774 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1777 return $new_priority; # so the caller knows what priority they wind up receiving
1780 =head1 AUTHOR
1782 Koha Development Team <http://koha-community.org/>
1784 =cut