X-Git-Url: http://koha-dev.rot13.org:8081/gitweb/?a=blobdiff_plain;f=C4%2FReserves.pm;h=276658ceed4304f6467e62145c240a18f7f418f0;hb=26bee7eee7d3e4602bc5e757278f600224fcbdf5;hp=7b9345aa2be6af15e46280851d0050d688dc38fa;hpb=ce1fbe07853720f3de43fa6fd5336575db6f575f;p=koha_gimpoz diff --git a/C4/Reserves.pm b/C4/Reserves.pm index 7b9345aa2b..276658ceed 100644 --- a/C4/Reserves.pm +++ b/C4/Reserves.pm @@ -1,11 +1,9 @@ -# -*- tab-width: 8 -*- -# NOTE: This file uses standard 8-character tabs - package C4::Reserves; # Copyright 2000-2002 Katipo Communications # 2006 SAN Ouest Provence -# 2007 BibLibre Paul POULAIN +# 2007-2010 BibLibre Paul POULAIN +# 2011 Catalyst IT # # This file is part of Koha. # @@ -18,22 +16,29 @@ package C4::Reserves; # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License along with -# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place, -# Suite 330, Boston, MA 02111-1307 USA +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. use strict; +#use warnings; FIXME - Bug 2505 use C4::Context; use C4::Biblio; +use C4::Members; use C4::Items; -use C4::Search; use C4::Circulation; use C4::Accounts; -use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); +# for _koha_notify_reserve +use C4::Members::Messaging; +use C4::Members qw(); +use C4::Letters; +use C4::Branch qw( GetBranchDetail ); +use C4::Dates qw( format_date_in_iso ); +use List::MoreUtils qw( firstidx ); -my $library_name = C4::Context->preference("LibraryName"); +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); =head1 NAME @@ -45,14 +50,15 @@ C4::Reserves - Koha functions for dealing with reservation. =head1 DESCRIPTION - this modules provides somes functions to deal with reservations. - +This modules provides somes functions to deal with reservations. + Reserves are stored in reserves table. The following columns contains important values : - priority >0 : then the reserve is at 1st stage, and not yet affected to any item. =0 : then the reserve is being dealed - found : NULL : means the patron requested the 1st available, and we haven't choosen the item - W(aiting) : the reserve has an itemnumber affected, and is on the way + T(ransit) : the reserve is linked to an item but is in transit to the pickup branch + W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf F(inished) : the reserve has been completed, and is done - itemnumber : empty : the reserve is still unaffected to an item filled: the reserve is attached to an item @@ -62,27 +68,25 @@ C4::Reserves - Koha functions for dealing with reservation. a library having it run "transfertodo", and clic on the list if there is no transfer to do, the reserve waiting patron can pick it up P =0, F=W, I=filled - if there is a transfer to do, write in branchtransfer P =0, F=NULL, I=filled + if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled The pickup library recieve the book, it check in P =0, F=W, I=filled The patron borrow the book P =0, F=F, I=filled ==== 2nd use case ==== patron requests a document, a given item, If pickup is holding branch P =0, F=W, I=filled - If transfer needed, write in branchtransfer P =0, F=NULL, I=filled - The pickup library recieve the book, it checks it in P =0, F=W, I=filled + If transfer needed, write in branchtransfer P =0, F=T, I=filled + The pickup library receive the book, it checks it in P =0, F=W, I=filled The patron borrow the book P =0, F=F, I=filled - -=head1 FUNCTIONS -=over 2 +=head1 FUNCTIONS =cut BEGIN { # set the version for version checking $VERSION = 3.01; - require Exporter; + require Exporter; @ISA = qw(Exporter); @EXPORT = qw( &AddReserve @@ -94,8 +98,9 @@ BEGIN { &GetReservesToBranch &GetReserveCount &GetReserveFee - &GetReserveInfo - + &GetReserveInfo + &GetReserveStatus + &GetOtherReserves &ModReserveFill @@ -104,22 +109,34 @@ BEGIN { &ModReserveStatus &ModReserveCancelAll &ModReserveMinusPriority + &MoveReserve &CheckReserves + &CanBookBeReserved + &CanItemBeReserved &CancelReserve + &CancelExpiredReserves + + &IsAvailableForItemLevelRequest + + &AlterPriority + &ToggleLowestPriority + + &ReserveSlip ); + @EXPORT_OK = qw( MergeHolds ); } -=item AddReserve +=head2 AddReserve - AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$notes,$title,$checkitem,$found) + AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found) =cut sub AddReserve { my ( $branch, $borrowernumber, $biblionumber, - $constraint, $bibitems, $priority, $notes, + $constraint, $bibitems, $priority, $resdate, $expdate, $notes, $title, $checkitem, $found ) = @_; my $fee = @@ -127,9 +144,17 @@ sub AddReserve { $bibitems ); my $dbh = C4::Context->dbh; my $const = lc substr( $constraint, 0, 1 ); - my @datearr = localtime(time); - my $resdate = - ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3]; + $resdate = format_date_in_iso( $resdate ) if ( $resdate ); + $resdate = C4::Dates->today( 'iso' ) unless ( $resdate ); + if ($expdate) { + $expdate = format_date_in_iso( $expdate ); + } else { + undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00' + } + if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) { + # Make room in reserves for this before those of a later reserve date + $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority ); + } my $waitingdate; # If the reserv had the waiting status, we had the value of the resdate @@ -150,60 +175,80 @@ sub AddReserve { my $usth = $dbh->prepare($query); $usth->execute( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ); - $usth->finish; } #if ($const eq 'a'){ my $query = qq/ INSERT INTO reserves (borrowernumber,biblionumber,reservedate,branchcode,constrainttype, - priority,reservenotes,itemnumber,found,waitingdate) + priority,reservenotes,itemnumber,found,waitingdate,expirationdate) VALUES (?,?,?,?,?, - ?,?,?,?,?) + ?,?,?,?,?,?) /; my $sth = $dbh->prepare($query); $sth->execute( $borrowernumber, $biblionumber, $resdate, $branch, $const, $priority, $notes, $checkitem, - $found, $waitingdate + $found, $waitingdate, $expdate ); - $sth->finish; + + # Send e-mail to librarian if syspref is active + if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){ + my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); + my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode}); + if ( my $letter = C4::Letters::GetPreparedLetter ( + module => 'reserves', + letter_code => 'HOLDPLACED', + branchcode => $branch, + tables => { + 'branches' => $branch_details, + 'borrowers' => $borrower, + 'biblio' => $biblionumber, + }, + ) ) { + + my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress'); + + C4::Letters::EnqueueLetter( + { letter => $letter, + borrowernumber => $borrowernumber, + message_transport_type => 'email', + from_address => $admin_email_address, + to_address => $admin_email_address, + } + ); + } + } #} - if ( ( $const eq "o" ) || ( $const eq "e" ) ) { - my $numitems = @$bibitems; - my $i = 0; - while ( $i < $numitems ) { - my $biblioitem = @$bibitems[$i]; - my $query = qq/ - INSERT INTO reserveconstraints - (borrowernumber,biblionumber,reservedate,biblioitemnumber) - VALUES + ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value? + $query = qq/ + INSERT INTO reserveconstraints + (borrowernumber,biblionumber,reservedate,biblioitemnumber) + VALUES (?,?,?,?) - /; - my $sth = $dbh->prepare(""); - $sth->execute( $borrowernumber, $biblionumber, $resdate, - $biblioitem ); - $sth->finish; - $i++; - } + /; + $sth = $dbh->prepare($query); # keep prepare outside the loop! + foreach (@$bibitems) { + $sth->execute($borrowernumber, $biblionumber, $resdate, $_); } - return; + + return; # FIXME: why not have a useful return value? } -=item GetReservesFromBiblionumber +=head2 GetReservesFromBiblionumber -@borrowerreserv=&GetReserves($biblionumber,$itemnumber,$borrowernumber); + ($count, $title_reserves) = &GetReserves($biblionumber); -this function get the list of reservation for an C<$biblionumber>, C<$itemnumber> or C<$borrowernumber> -given on input arg. -Only 1 argument has to be passed. +This function gets the list of reservations for one C<$biblionumber>, returning a count +of the reserves and an arrayref pointing to the reserves for C<$biblionumber>. =cut sub GetReservesFromBiblionumber { - my ( $biblionumber, $itemnumber, $borrowernumber ) = @_; + my ($biblionumber) = shift or return (0, []); + my ($all_dates) = shift; my $dbh = C4::Context->dbh; # Find the desired items in the reserves @@ -217,49 +262,49 @@ sub GetReservesFromBiblionumber { constrainttype, found, itemnumber, - reservenotes + reservenotes, + expirationdate, + lowestPriority FROM reserves - WHERE biblionumber = ? - ORDER BY priority"; + WHERE biblionumber = ? "; + unless ( $all_dates ) { + $query .= "AND reservedate <= CURRENT_DATE()"; + } + $query .= "ORDER BY priority"; my $sth = $dbh->prepare($query); $sth->execute($biblionumber); my @results; my $i = 0; while ( my $data = $sth->fetchrow_hashref ) { - # FIXME - What is this if-statement doing? How do constraints work? - if ( $data->{constrainttype} eq 'o' ) { + # FIXME - What is this doing? How do constraints work? + if ($data->{constrainttype} eq 'o') { $query = ' SELECT biblioitemnumber - FROM reserveconstraints - WHERE biblionumber = ? - AND borrowernumber = ? - AND reservedate = ? + FROM reserveconstraints + WHERE biblionumber = ? + AND borrowernumber = ? + AND reservedate = ? '; my $csth = $dbh->prepare($query); - $csth->execute( $data->{biblionumber}, $data->{borrowernumber}, - $data->{reservedate}, ); - + $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate}); my @bibitemno; while ( my $bibitemnos = $csth->fetchrow_array ) { - push( @bibitemno, $bibitemnos ); + push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref } - my $count = @bibitemno; - + my $count = scalar @bibitemno; + # if we have two or more different specific itemtypes # reserved by same person on same day my $bdata; if ( $count > 1 ) { - $bdata = GetBiblioItemData( $bibitemno[$i] ); - $i++; + $bdata = GetBiblioItemData( $bibitemno[$i] ); # FIXME: This doesn't make sense. + $i++; # $i can increase each pass, but the next @bibitemno might be smaller? } else { - # Look up the book we just found. $bdata = GetBiblioItemData( $bibitemno[0] ); } - $csth->finish; - # Add the results of this latest search to the current # results. # FIXME - An 'each' would probably be more efficient. @@ -269,38 +314,40 @@ sub GetReservesFromBiblionumber { } push @results, $data; } - $sth->finish; return ( $#results + 1, \@results ); } -=item GetReservesFromItemnumber +=head2 GetReservesFromItemnumber ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber); - TODO :: Description here +TODO :: Description here =cut sub GetReservesFromItemnumber { - my ( $itemnumber ) = @_; + my ( $itemnumber, $all_dates ) = @_; my $dbh = C4::Context->dbh; my $query = " SELECT reservedate,borrowernumber,branchcode FROM reserves WHERE itemnumber=? "; + unless ( $all_dates ) { + $query .= " AND reservedate <= CURRENT_DATE()"; + } my $sth_res = $dbh->prepare($query); $sth_res->execute($itemnumber); my ( $reservedate, $borrowernumber,$branchcode ) = $sth_res->fetchrow_array; return ( $reservedate, $borrowernumber, $branchcode ); } -=item GetReservesFromBorrowernumber +=head2 GetReservesFromBorrowernumber $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus); - - TODO :: Descritpion - + +TODO :: Descritpion + =cut sub GetReservesFromBorrowernumber { @@ -329,10 +376,122 @@ sub GetReservesFromBorrowernumber { return @$data; } #------------------------------------------------------------------------------------- +=head2 CanBookBeReserved -=item GetReserveCount + $error = &CanBookBeReserved($borrowernumber, $biblionumber) -$number = &GetReserveCount($borrowernumber); +=cut + +sub CanBookBeReserved{ + my ($borrowernumber, $biblionumber) = @_; + + my @items = get_itemnumbers_of($biblionumber); + #get items linked via host records + my @hostitems = get_hostitemnumbers_of($biblionumber); + if (@hostitems){ + push (@items,@hostitems); + } + + foreach my $item (@items){ + return 1 if CanItemBeReserved($borrowernumber, $item); + } + return 0; +} + +=head2 CanItemBeReserved + + $error = &CanItemBeReserved($borrowernumber, $itemnumber) + +This function return 1 if an item can be issued by this borrower. + +=cut + +sub CanItemBeReserved{ + my ($borrowernumber, $itemnumber) = @_; + + my $dbh = C4::Context->dbh; + my $allowedreserves = 0; + + my $controlbranch = C4::Context->preference('ReservesControlBranch'); + my $itype = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype"; + + # we retrieve borrowers and items informations # + my $item = GetItem($itemnumber); + my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber); + + # we retrieve user rights on this itemtype and branchcode + my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed + FROM issuingrules + WHERE (categorycode in (?,'*') ) + AND (itemtype IN (?,'*')) + AND (branchcode IN (?,'*')) + ORDER BY + categorycode DESC, + itemtype DESC, + branchcode DESC;" + ); + + my $querycount ="SELECT + count(*) as count + FROM reserves + LEFT JOIN items USING (itemnumber) + LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber) + LEFT JOIN borrowers USING (borrowernumber) + WHERE borrowernumber = ? + "; + + + my $itemtype = $item->{$itype}; + my $categorycode = $borrower->{categorycode}; + my $branchcode = ""; + my $branchfield = "reserves.branchcode"; + + if( $controlbranch eq "ItemHomeLibrary" ){ + $branchfield = "items.homebranch"; + $branchcode = $item->{homebranch}; + }elsif( $controlbranch eq "PatronLibrary" ){ + $branchfield = "borrowers.branchcode"; + $branchcode = $borrower->{branchcode}; + } + + # we retrieve rights + $sth->execute($categorycode, $itemtype, $branchcode); + if(my $rights = $sth->fetchrow_hashref()){ + $itemtype = $rights->{itemtype}; + $allowedreserves = $rights->{reservesallowed}; + }else{ + $itemtype = '*'; + } + + # we retrieve count + + $querycount .= "AND $branchfield = ?"; + + $querycount .= " AND $itype = ?" if ($itemtype ne "*"); + my $sthcount = $dbh->prepare($querycount); + + if($itemtype eq "*"){ + $sthcount->execute($borrowernumber, $branchcode); + }else{ + $sthcount->execute($borrowernumber, $branchcode, $itemtype); + } + + my $reservecount = "0"; + if(my $rowcount = $sthcount->fetchrow_hashref()){ + $reservecount = $rowcount->{count}; + } + + # we check if it's ok or not + if( $reservecount < $allowedreserves ){ + return 1; + }else{ + return 0; + } +} +#-------------------------------------------------------------------------------- +=head2 GetReserveCount + + $number = &GetReserveCount($borrowernumber); this function returns the number of reservation for a borrower given on input arg. @@ -351,14 +510,12 @@ sub GetReserveCount { my $sth = $dbh->prepare($query); $sth->execute($borrowernumber); my $row = $sth->fetchrow_hashref; - $sth->finish; - return $row->{counter}; } -=item GetOtherReserves +=head2 GetOtherReserves -($messages,$nextreservinfo)=$GetOtherReserves(itemnumber); + ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber); Check queued list of this document and check if this document must be transfered @@ -368,7 +525,7 @@ sub GetOtherReserves { my ($itemnumber) = @_; my $messages; my $nextreservinfo; - my ( $restype, $checkreserves ) = CheckReserves($itemnumber); + my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber); if ($checkreserves) { my $iteminfo = GetItem($itemnumber); if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) { @@ -381,7 +538,7 @@ sub GetOtherReserves { ); #launch the subroutine dotransfer - C4::Circulation::ModItemTransfer( + C4::Items::ModItemTransfer( $itemnumber, $iteminfo->{'holdingbranch'}, $checkreserves->{'branchcode'} @@ -406,9 +563,9 @@ sub GetOtherReserves { return ( $messages, $nextreservinfo ); } -=item GetReserveFee +=head2 GetReserveFee -$fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber); + $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber); Calculate the fee for a reserve @@ -509,9 +666,9 @@ sub GetReserveFee { return $fee; } -=item GetReservesToBranch +=head2 GetReservesToBranch -@transreserv = GetReservesToBranch( $frombranch ); + @transreserv = GetReservesToBranch( $frombranch ); Get reserve list for a given branch @@ -533,13 +690,12 @@ sub GetReservesToBranch { $transreserv[$i] = $data; $i++; } - $sth->finish; return (@transreserv); } -=item GetReservesForBranch +=head2 GetReservesForBranch -@transreserv = GetReservesForBranch($frombranch); + @transreserv = GetReservesForBranch($frombranch); =cut @@ -567,13 +723,25 @@ sub GetReservesForBranch { $transreserv[$i] = $data; $i++; } - $sth->finish; return (@transreserv); } -=item CheckReserves +sub GetReserveStatus { + my ($itemnumber) = @_; + + my $dbh = C4::Context->dbh; + + my $itemstatus = $dbh->prepare("SELECT found FROM reserves WHERE itemnumber = ?"); + + $itemstatus->execute($itemnumber); + my ($found) = $itemstatus->fetchrow_array; + return $found; +} - ($status, $reserve) = &CheckReserves($itemnumber); +=head2 CheckReserves + + ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber); + ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode); Find a book in the reserves. @@ -601,65 +769,71 @@ sub CheckReserves { my ( $item, $barcode ) = @_; my $dbh = C4::Context->dbh; my $sth; - if ($item) { - my $qitem = $dbh->quote($item); - # Look up the item by itemnumber - my $query = " - SELECT items.biblionumber, items.biblioitemnumber, itemtypes.notforloan - FROM items - LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber - LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype - WHERE itemnumber=$qitem + my $select; + if (C4::Context->preference('item-level_itypes')){ + $select = " + SELECT items.biblionumber, + items.biblioitemnumber, + itemtypes.notforloan, + items.notforloan AS itemnotforloan, + items.itemnumber + FROM items + LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber + LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype "; - $sth = $dbh->prepare($query); } else { - my $qbc = $dbh->quote($barcode); - # Look up the item by barcode - my $query = " - SELECT items.biblionumber, items.biblioitemnumber, itemtypes.notforloan - FROM items - LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber - LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype - WHERE items.biblioitemnumber = biblioitems.biblioitemnumber - AND biblioitems.itemtype = itemtypes.itemtype - AND barcode=$qbc + $select = " + SELECT items.biblionumber, + items.biblioitemnumber, + itemtypes.notforloan, + items.notforloan AS itemnotforloan, + items.itemnumber + FROM items + LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber + LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype "; - $sth = $dbh->prepare($query); - - # FIXME - This function uses $item later on. Ought to set it here. } - $sth->execute; - my ( $biblio, $bibitem, $notforloan ) = $sth->fetchrow_array; - $sth->finish; + + if ($item) { + $sth = $dbh->prepare("$select WHERE itemnumber = ?"); + $sth->execute($item); + } + else { + $sth = $dbh->prepare("$select WHERE barcode = ?"); + $sth->execute($barcode); + } + # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it. + my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array; + + return ( '' ) unless $itemnumber; # bail if we got nothing. # if item is not for loan it cannot be reserved either..... - return ( 0, 0 ) if $notforloan; + # execpt where items.notforloan < 0 : This indicates the item is holdable. + return ( '' ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype; - # get the reserves... # Find this item in the reserves - my @reserves = _Findgroupreserve( $bibitem, $biblio, $item ); - my $count = scalar @reserves; + my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber ); # $priority and $highest are used to find the most important item # in the list returned by &_Findgroupreserve. (The lower $priority, # the more important the item.) # $highest is the most important item we've seen so far. - my $priority = 10000000; my $highest; - if ($count) { + if (scalar @reserves) { + my $priority = 10000000; foreach my $res (@reserves) { - # FIXME - $item might be undefined or empty: the caller - # might be searching by barcode. - if ( $res->{'itemnumber'} == $item ) { - # Found it - return ( "Waiting", $res ); - } - else { - # See if this item is more important than what we've got - # so far. - if ( $res->{'priority'} != 0 && $res->{'priority'} < $priority ) - { + if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) { + return ( "Waiting", $res, \@reserves ); # Found it + } else { + # See if this item is more important than what we've got so far + if ( $res->{'priority'} && $res->{'priority'} < $priority ) { + my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'}); + my $iteminfo=C4::Items::GetItem($itemnumber); + my $branch=C4::Circulation::_GetCircControlBranch($iteminfo,$borrowerinfo); + my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'}); + next if ($branchitemrule->{'holdallowed'} == 0); + next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'})); $priority = $res->{'priority'}; $highest = $res; } @@ -667,27 +841,68 @@ sub CheckReserves { } } - # If we get this far, then no exact match was found. Print the - # most important item on the list. I think this tells us who's - # next in line to get this book. - if ($highest) { # FIXME - $highest might be undefined + # If we get this far, then no exact match was found. + # We return the most important (i.e. next) reservation. + if ($highest) { $highest->{'itemnumber'} = $item; - return ( "Reserved", $highest ); + return ( "Reserved", $highest, \@reserves ); } - else { - return ( 0, 0 ); + + return ( '' ); +} + +=head2 CancelExpiredReserves + + CancelExpiredReserves(); + +Cancels all reserves with an expiration date from before today. + +=cut + +sub CancelExpiredReserves { + + # Cancel reserves that have passed their expiration date. + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare( " + SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() ) + AND expirationdate IS NOT NULL + AND found IS NULL + " ); + $sth->execute(); + + while ( my $res = $sth->fetchrow_hashref() ) { + CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} ); } + + # Cancel reserves that have been waiting too long + if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) { + my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay"); + my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge"); + + my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0"; + $sth = $dbh->prepare( $query ); + $sth->execute( $max_pickup_delay ); + + while (my $res = $sth->fetchrow_hashref ) { + if ( $charge ) { + manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge); + } + + CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} ); + } + } + } -=item CancelReserve +=head2 CancelReserve &CancelReserve($biblionumber, $itemnumber, $borrowernumber); Cancels a reserve. Use either C<$biblionumber> or C<$itemnumber> to specify the item to -cancel, but not both: if both are given, C<&CancelReserve> does -nothing. +cancel, but not both: if both are given, C<&CancelReserve> uses +C<$itemnumber>. C<$borrowernumber> is the borrower number of the patron on whose behalf the book was reserved. @@ -777,13 +992,38 @@ sub CancelReserve { $sth->execute( $biblio, $borr ); # now fix the priority on the others.... - _FixPriority( $priority, $biblio ); + _FixPriority( $biblio, $borr ); } } -=item ModReserve +=head2 ModReserve + + ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber]) + +Change a hold request's priority or cancel it. + +C<$rank> specifies the effect of the change. If C<$rank> +is 'W' or 'n', nothing happens. This corresponds to leaving a +request alone when changing its priority in the holds queue +for a bib. + +If C<$rank> is 'del', the hold request is cancelled. -&ModReserve($rank,$biblio,$borrower,$branch) +If C<$rank> is an integer greater than zero, the priority of +the request is set to that value. Since priority != 0 means +that the item is not waiting on the hold shelf, setting the +priority to a non-zero value also sets the request's found +status and waiting date to NULL. + +The optional C<$itemnumber> parameter is used only when +C<$rank> is a non-zero integer; if supplied, the itemnumber +of the hold request is set accordingly; if omitted, the itemnumber +is cleared. + +B Note that the forgoing can have the effect of causing +item-level hold requests to turn into title-level requests. This +will be fixed once reserves has separate columns for requested +itemnumber and supplying itemnumber. =cut @@ -821,9 +1061,9 @@ sub ModReserve { $sth->execute( $biblio, $borrower ); } - else { + elsif ($rank =~ /^\d+/ and $rank > 0) { my $query = qq/ - UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL + UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL WHERE biblionumber = ? AND borrowernumber = ? /; @@ -834,7 +1074,7 @@ sub ModReserve { } } -=item ModReserveFill +=head2 ModReserveFill &ModReserveFill($reserve); @@ -898,13 +1138,13 @@ sub ModReserveFill { # now fix the priority on the others (if the priority wasn't # already sorted!).... unless ( $priority == 0 ) { - _FixPriority( $priority, $biblionumber ); + _FixPriority( $biblionumber, $borrowernumber ); } } -=item ModReserveStatus +=head2 ModReserveStatus -&ModReserveStatus($itemnumber, $newstatus); + &ModReserveStatus($itemnumber, $newstatus); Update the reserve status for the active (priority=0) reserve. @@ -918,21 +1158,20 @@ sub ModReserveStatus { #first : check if we have a reservation for this item . my ($itemnumber, $newstatus) = @_; - my $dbh = C4::Context->dbh; - my $query = " UPDATE reserves - SET found=?,waitingdate = now() - WHERE itemnumber=? - AND found IS NULL - AND priority = 0 - "; + my $dbh = C4::Context->dbh; + + my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0"; my $sth_set = $dbh->prepare($query); $sth_set->execute( $newstatus, $itemnumber ); - $sth_set->finish; + + if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) { + CartToShelf( $itemnumber ); + } } -=item ModReserveAffect +=head2 ModReserveAffect -&ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend); + &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend); This function affect an item and a status for a given reserve The itemnumber parameter is used to find the biblionumber. @@ -942,6 +1181,7 @@ to the correct reserve. if $transferToDo is not set, then the status is set to "Waiting" as well. otherwise, a transfer is on the way, and the end of the transfer will take care of the waiting status + =cut sub ModReserveAffect { @@ -953,40 +1193,52 @@ sub ModReserveAffect { my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?"); $sth->execute($itemnumber); my ($biblionumber) = $sth->fetchrow; + + # get request - need to find out if item is already + # waiting in order to not send duplicate hold filled notifications + my $request = GetReserveInfo($borrowernumber, $biblionumber); + my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0; + # If we affect a reserve that has to be transfered, don't set to Waiting my $query; if ($transferToDo) { $query = " UPDATE reserves SET priority = 0, - itemnumber = ? + itemnumber = ?, + found = 'T' WHERE borrowernumber = ? AND biblionumber = ? "; } else { # affect the reserve to Waiting as well. - $query = " - UPDATE reserves - SET priority = 0, - found = 'W', - waitingdate=now(), - itemnumber = ? - WHERE borrowernumber = ? - AND biblionumber = ? - "; + $query = " + UPDATE reserves + SET priority = 0, + found = 'W', + waitingdate = NOW(), + itemnumber = ? + WHERE borrowernumber = ? + AND biblionumber = ? + "; } $sth = $dbh->prepare($query); $sth->execute( $itemnumber, $borrowernumber,$biblionumber); - $sth->finish; + _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf ); + + if ( C4::Context->preference("ReturnToShelvingCart") ) { + CartToShelf( $itemnumber ); + } + return; } -=item ModReserveCancelAll +=head2 ModReserveCancelAll -($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber); + ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber); - function to cancel reserv,check other reserves, and transfer document if it's necessary +function to cancel reserv,check other reserves, and transfer document if it's necessary =cut @@ -1004,9 +1256,9 @@ sub ModReserveCancelAll { return ( $messages, $nextreservinfo ); } -=item ModReserveMinusPriority +=head2 ModReserveMinusPriority -&ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber) + &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber) Reduce the values of queuded list @@ -1025,48 +1277,57 @@ sub ModReserveMinusPriority { "; my $sth_upd = $dbh->prepare($query); $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber ); - $sth_upd->finish; # second step update all others reservs - $query = " - UPDATE reserves - SET priority = priority-1 - WHERE biblionumber = ? - AND priority > 0 - "; - $sth_upd = $dbh->prepare($query); - $sth_upd->execute( $biblionumber ); - $sth_upd->finish; - $sth_upd->finish; + _FixPriority($biblionumber, $borrowernumber, '0'); } -=item GetReserveInfo +=head2 GetReserveInfo -&GetReserveInfo($borrowernumber,$biblionumber); + &GetReserveInfo($borrowernumber,$biblionumber); + +Get item and borrower details for a current hold. +Current implementation this query should have a single result. - Get item and borrower details for a current hold. - Current implementation this query should have a single result. =cut sub GetReserveInfo { my ( $borrowernumber, $biblionumber ) = @_; my $dbh = C4::Context->dbh; - my $strsth="SELECT reservedate, reservenotes, reserves.borrowernumber, - reserves.biblionumber, reserves.branchcode, - notificationdate, reminderdate, priority, found, - firstname, surname, phone, - email, address, address2, - cardnumber, city, zipcode, - biblio.title, biblio.author, - items.holdingbranch, items.itemcallnumber, items.itemnumber, - barcode, notes - FROM reserves left join items - ON items.itemnumber=reserves.itemnumber , - borrowers, biblio + my $strsth="SELECT + reservedate, + reservenotes, + reserves.borrowernumber, + reserves.biblionumber, + reserves.branchcode, + reserves.waitingdate, + notificationdate, + reminderdate, + priority, + found, + firstname, + surname, + phone, + email, + address, + address2, + cardnumber, + city, + zipcode, + biblio.title, + biblio.author, + items.holdingbranch, + items.itemcallnumber, + items.itemnumber, + items.location, + barcode, + notes + FROM reserves + LEFT JOIN items USING(itemnumber) + LEFT JOIN borrowers USING(borrowernumber) + LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber) WHERE - reserves.borrowernumber=? && - reserves.biblionumber=? && - reserves.borrowernumber=borrowers.borrowernumber && - reserves.biblionumber=biblio.biblionumber "; + reserves.borrowernumber=? + AND reserves.biblionumber=?"; my $sth = $dbh->prepare($strsth); $sth->execute($borrowernumber,$biblionumber); @@ -1075,34 +1336,170 @@ sub GetReserveInfo { } -=item _FixPriority +=head2 IsAvailableForItemLevelRequest + + my $is_available = IsAvailableForItemLevelRequest($itemnumber); + +Checks whether a given item record is available for an +item-level hold request. An item is available if + +* it is not lost AND +* it is not damaged AND +* it is not withdrawn AND +* does not have a not for loan value > 0 + +Whether or not the item is currently on loan is +also checked - if the AllowOnShelfHolds system preference +is ON, an item can be requested even if it is currently +on loan to somebody else. If the system preference +is OFF, an item that is currently checked out cannot +be the target of an item-level hold request. + +Note that IsAvailableForItemLevelRequest() does not +check if the staff operator is authorized to place +a request on the item - in particular, +this routine does not check IndependantBranches +and canreservefromotherbranches. + +=cut + +sub IsAvailableForItemLevelRequest { + my $itemnumber = shift; + + my $item = GetItem($itemnumber); + + # must check the notforloan setting of the itemtype + # FIXME - a lot of places in the code do this + # or something similar - need to be + # consolidated + my $dbh = C4::Context->dbh; + my $notforloan_query; + if (C4::Context->preference('item-level_itypes')) { + $notforloan_query = "SELECT itemtypes.notforloan + FROM items + JOIN itemtypes ON (itemtypes.itemtype = items.itype) + WHERE itemnumber = ?"; + } else { + $notforloan_query = "SELECT itemtypes.notforloan + FROM items + JOIN biblioitems USING (biblioitemnumber) + JOIN itemtypes USING (itemtype) + WHERE itemnumber = ?"; + } + my $sth = $dbh->prepare($notforloan_query); + $sth->execute($itemnumber); + my $notforloan_per_itemtype = 0; + if (my ($notforloan) = $sth->fetchrow_array) { + $notforloan_per_itemtype = 1 if $notforloan; + } + + my $available_per_item = 1; + $available_per_item = 0 if $item->{itemlost} or + ( $item->{notforloan} > 0 ) or + ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or + $item->{wthdrawn} or + $notforloan_per_itemtype; + + + if (C4::Context->preference('AllowOnShelfHolds')) { + return $available_per_item; + } else { + return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "W")); + } +} + +=head2 AlterPriority + + AlterPriority( $where, $borrowernumber, $biblionumber, $reservedate ); + +This function changes a reserve's priority up, down, to the top, or to the bottom. +Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed + +=cut + +sub AlterPriority { + my ( $where, $borrowernumber, $biblionumber ) = @_; + + my $dbh = C4::Context->dbh; + + ## Find this reserve + my $sth = $dbh->prepare('SELECT * FROM reserves WHERE biblionumber = ? AND borrowernumber = ? AND cancellationdate IS NULL'); + $sth->execute( $biblionumber, $borrowernumber ); + my $reserve = $sth->fetchrow_hashref(); + $sth->finish(); + + if ( $where eq 'up' || $where eq 'down' ) { + + my $priority = $reserve->{'priority'}; + $priority = $where eq 'up' ? $priority - 1 : $priority + 1; + _FixPriority( $biblionumber, $borrowernumber, $priority ) + + } elsif ( $where eq 'top' ) { + + _FixPriority( $biblionumber, $borrowernumber, '1' ) + + } elsif ( $where eq 'bottom' ) { + + _FixPriority( $biblionumber, $borrowernumber, '999999' ) + + } +} + +=head2 ToggleLowestPriority + + ToggleLowestPriority( $borrowernumber, $biblionumber ); + +This function sets the lowestPriority field to true if is false, and false if it is true. + +=cut + +sub ToggleLowestPriority { + my ( $borrowernumber, $biblionumber ) = @_; + + my $dbh = C4::Context->dbh; + + my $sth = $dbh->prepare( + "UPDATE reserves SET lowestPriority = NOT lowestPriority + WHERE biblionumber = ? + AND borrowernumber = ?" + ); + $sth->execute( + $biblionumber, + $borrowernumber, + ); + $sth->finish; + + _FixPriority( $biblionumber, $borrowernumber, '999999' ); +} + +=head2 _FixPriority -&_FixPriority($biblio,$borrowernumber,$rank); + &_FixPriority($biblio,$borrowernumber,$rank,$ignoreSetLowestRank); - Only used internally (so don't export it) - Changed how this functions works # - Now just gets an array of reserves in the rank order and updates them with - the array index (+1 as array starts from 0) - and if $rank is supplied will splice item from the array and splice it back in again - in new priority rank +Only used internally (so don't export it) +Changed how this functions works # +Now just gets an array of reserves in the rank order and updates them with +the array index (+1 as array starts from 0) +and if $rank is supplied will splice item from the array and splice it back in again +in new priority rank =cut sub _FixPriority { - my ( $biblio, $borrowernumber, $rank ) = @_; + my ( $biblio, $borrowernumber, $rank, $ignoreSetLowestRank ) = @_; my $dbh = C4::Context->dbh; if ( $rank eq "del" ) { CancelReserve( $biblio, undef, $borrowernumber ); } if ( $rank eq "W" || $rank eq "0" ) { - # make sure priority for waiting items is 0 + # make sure priority for waiting or in-transit items is 0 my $query = qq/ UPDATE reserves SET priority = 0 WHERE biblionumber = ? AND borrowernumber = ? - AND found ='W' + AND found IN ('W', 'T') /; my $sth = $dbh->prepare($query); $sth->execute( $biblio, $borrowernumber ); @@ -1119,7 +1516,7 @@ sub _FixPriority { SELECT borrowernumber, reservedate, constrainttype FROM reserves WHERE biblionumber = ? - AND ((found <> 'W') or found is NULL) + AND ((found <> 'W' AND found <> 'T') or found is NULL) ORDER BY priority ASC /; my $sth = $dbh->prepare($query); @@ -1165,18 +1562,26 @@ sub _FixPriority { ); $sth->finish; } + + $sth = $dbh->prepare( "SELECT borrowernumber FROM reserves WHERE lowestPriority = 1 ORDER BY priority" ); + $sth->execute(); + + unless ( $ignoreSetLowestRank ) { + while ( my $res = $sth->fetchrow_hashref() ) { + _FixPriority( $biblio, $res->{'borrowernumber'}, '999999', 1 ); + } + } } -=item _Findgroupreserve +=head2 _Findgroupreserve @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber); -****** FIXME ****** -I don't know what this does, because I don't understand how reserve -constraints work. I think the idea is that you reserve a particular -biblio, and the constraint allows you to restrict it to a given -biblioitem (e.g., if you want to borrow the audio book edition of "The -Prophet", rather than the first available publication). +Looks for an item-specific match first, then for a title-level match, returning the +first match found. If neither, then we look for a 3rd kind of match based on +reserve constraints. + +TODO: add more explanation about reserve constraints C<&_Findgroupreserve> returns : C<@results> is an array of references-to-hash whose keys are mostly @@ -1188,42 +1593,370 @@ C. sub _Findgroupreserve { my ( $bibitem, $biblio, $itemnumber ) = @_; my $dbh = C4::Context->dbh; + + # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var. + # check for exact targetted match + my $item_level_target_query = qq/ + SELECT reserves.biblionumber AS biblionumber, + reserves.borrowernumber AS borrowernumber, + reserves.reservedate AS reservedate, + reserves.branchcode AS branchcode, + reserves.cancellationdate AS cancellationdate, + reserves.found AS found, + reserves.reservenotes AS reservenotes, + reserves.priority AS priority, + reserves.timestamp AS timestamp, + biblioitems.biblioitemnumber AS biblioitemnumber, + reserves.itemnumber AS itemnumber + FROM reserves + JOIN biblioitems USING (biblionumber) + JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber) + WHERE found IS NULL + AND priority > 0 + AND item_level_request = 1 + AND itemnumber = ? + AND reservedate <= CURRENT_DATE() + /; + my $sth = $dbh->prepare($item_level_target_query); + $sth->execute($itemnumber); + my @results; + if ( my $data = $sth->fetchrow_hashref ) { + push( @results, $data ); + } + return @results if @results; + + # check for title-level targetted match + my $title_level_target_query = qq/ + SELECT reserves.biblionumber AS biblionumber, + reserves.borrowernumber AS borrowernumber, + reserves.reservedate AS reservedate, + reserves.branchcode AS branchcode, + reserves.cancellationdate AS cancellationdate, + reserves.found AS found, + reserves.reservenotes AS reservenotes, + reserves.priority AS priority, + reserves.timestamp AS timestamp, + biblioitems.biblioitemnumber AS biblioitemnumber, + reserves.itemnumber AS itemnumber + FROM reserves + JOIN biblioitems USING (biblionumber) + JOIN hold_fill_targets USING (biblionumber, borrowernumber) + WHERE found IS NULL + AND priority > 0 + AND item_level_request = 0 + AND hold_fill_targets.itemnumber = ? + AND reservedate <= CURRENT_DATE() + /; + $sth = $dbh->prepare($title_level_target_query); + $sth->execute($itemnumber); + @results = (); + if ( my $data = $sth->fetchrow_hashref ) { + push( @results, $data ); + } + return @results if @results; + my $query = qq/ - SELECT reserves.biblionumber AS biblionumber, - reserves.borrowernumber AS borrowernumber, - reserves.reservedate AS reservedate, - reserves.branchcode AS branchcode, - reserves.cancellationdate AS cancellationdate, - reserves.found AS found, - reserves.reservenotes AS reservenotes, - reserves.priority AS priority, - reserves.timestamp AS timestamp, + SELECT reserves.biblionumber AS biblionumber, + reserves.borrowernumber AS borrowernumber, + reserves.reservedate AS reservedate, + reserves.waitingdate AS waitingdate, + reserves.branchcode AS branchcode, + reserves.cancellationdate AS cancellationdate, + reserves.found AS found, + reserves.reservenotes AS reservenotes, + reserves.priority AS priority, + reserves.timestamp AS timestamp, reserveconstraints.biblioitemnumber AS biblioitemnumber, - reserves.itemnumber AS itemnumber + reserves.itemnumber AS itemnumber FROM reserves LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber WHERE reserves.biblionumber = ? AND ( ( reserveconstraints.biblioitemnumber = ? AND reserves.borrowernumber = reserveconstraints.borrowernumber - AND reserves.reservedate =reserveconstraints.reservedate ) + AND reserves.reservedate = reserveconstraints.reservedate ) OR reserves.constrainttype='a' ) AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?) + AND reserves.reservedate <= CURRENT_DATE() /; - my $sth = $dbh->prepare($query); + $sth = $dbh->prepare($query); $sth->execute( $biblio, $bibitem, $itemnumber ); - my @results; + @results = (); while ( my $data = $sth->fetchrow_hashref ) { push( @results, $data ); } - $sth->finish; return @results; } -=back +=head2 _koha_notify_reserve + + _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ); + +Sends a notification to the patron that their hold has been filled (through +ModReserveAffect, _not_ ModReserveFill) + +=cut + +sub _koha_notify_reserve { + my ($itemnumber, $borrowernumber, $biblionumber) = @_; + + my $dbh = C4::Context->dbh; + my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); + + # Try to get the borrower's email address + my $to_address; + my $which_address = C4::Context->preference('AutoEmailPrimaryAddress'); + # If the system preference is set to 'first valid' (value == OFF), look up email address + if ($which_address eq 'OFF') { + $to_address = C4::Members::GetFirstValidEmailAddress( $borrowernumber ); + } else { + $to_address = $borrower->{$which_address}; + } + + my $letter_code; + my $print_mode = 0; + my $messagingprefs; + if ( $to_address || $borrower->{'smsalertnumber'} ) { + $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } ); + + return if ( !defined( $messagingprefs->{'letter_code'} ) ); + $letter_code = $messagingprefs->{'letter_code'}; + } else { + $letter_code = 'HOLD_PRINT'; + $print_mode = 1; + } + + my $sth = $dbh->prepare(" + SELECT * + FROM reserves + WHERE borrowernumber = ? + AND biblionumber = ? + "); + $sth->execute( $borrowernumber, $biblionumber ); + my $reserve = $sth->fetchrow_hashref; + my $branch_details = GetBranchDetail( $reserve->{'branchcode'} ); + + my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress'); + + my $letter = C4::Letters::GetPreparedLetter ( + module => 'reserves', + letter_code => $letter_code, + branchcode => $reserve->{branchcode}, + tables => { + 'branches' => $branch_details, + 'borrowers' => $borrower, + 'biblio' => $biblionumber, + 'reserves' => $reserve, + 'items', $reserve->{'itemnumber'}, + }, + substitute => { today => C4::Dates->new()->output() }, + ) or die "Could not find a letter called '$letter_code' in the 'reserves' module"; + + + + if ( $print_mode ) { + C4::Letters::EnqueueLetter( { + letter => $letter, + borrowernumber => $borrowernumber, + message_transport_type => 'print', + } ); + + return; + } + + if ( grep { $_ eq 'email' } @{$messagingprefs->{transports}} ) { + # aka, 'email' in ->{'transports'} + C4::Letters::EnqueueLetter( + { letter => $letter, + borrowernumber => $borrowernumber, + message_transport_type => 'email', + from_address => $admin_email_address, + } + ); + } + + if ( grep { $_ eq 'sms' } @{$messagingprefs->{transports}} ) { + C4::Letters::EnqueueLetter( + { letter => $letter, + borrowernumber => $borrowernumber, + message_transport_type => 'sms', + } + ); + } +} + +=head2 _ShiftPriorityByDateAndPriority + + $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority ); + +This increments the priority of all reserves after the one +with either the lowest date after C<$reservedate> +or the lowest priority after C<$priority>. + +It effectively makes room for a new reserve to be inserted with a certain +priority, which is returned. + +This is most useful when the reservedate can be set by the user. It allows +the new reserve to be placed before other reserves that have a later +reservedate. Since priority also is set by the form in reserves/request.pl +the sub accounts for that too. + +=cut + +sub _ShiftPriorityByDateAndPriority { + my ( $biblio, $resdate, $new_priority ) = @_; + + my $dbh = C4::Context->dbh; + my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1"; + my $sth = $dbh->prepare( $query ); + $sth->execute( $biblio, $resdate, $new_priority ); + my $min_priority = $sth->fetchrow; + # if no such matches are found, $new_priority remains as original value + $new_priority = $min_priority if ( $min_priority ); + + # Shift the priority up by one; works in conjunction with the next SQL statement + $query = "UPDATE reserves + SET priority = priority+1 + WHERE biblionumber = ? + AND borrowernumber = ? + AND reservedate = ? + AND found IS NULL"; + my $sth_update = $dbh->prepare( $query ); + + # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least + $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC"; + $sth = $dbh->prepare( $query ); + $sth->execute( $new_priority, $biblio ); + while ( my $row = $sth->fetchrow_hashref ) { + $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} ); + } + + return $new_priority; # so the caller knows what priority they wind up receiving +} + +=head2 MoveReserve + + MoveReserve( $itemnumber, $borrowernumber, $cancelreserve ) + +Use when checking out an item to handle reserves +If $cancelreserve boolean is set to true, it will remove existing reserve + +=cut + +sub MoveReserve { + my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_; + + my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber ); + return unless $res; + + my $biblionumber = $res->{biblionumber}; + my $biblioitemnumber = $res->{biblioitemnumber}; + + if ($res->{borrowernumber} == $borrowernumber) { + ModReserveFill($res); + } + else { + # warn "Reserved"; + # The item is reserved by someone else. + # Find this item in the reserves + + my $borr_res; + foreach (@$all_reserves) { + $_->{'borrowernumber'} == $borrowernumber or next; + $_->{'biblionumber'} == $biblionumber or next; + + $borr_res = $_; + last; + } + + if ( $borr_res ) { + # The item is reserved by the current patron + ModReserveFill($borr_res); + } + + if ($cancelreserve) { # cancel reserves on this item + CancelReserve(0, $res->{'itemnumber'}, $res->{'borrowernumber'}); + CancelReserve($res->{'biblionumber'}, 0, $res->{'borrowernumber'}); + } + } +} + +=head2 MergeHolds + + MergeHolds($dbh,$to_biblio, $from_biblio); + +This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed + +=cut + +sub MergeHolds { + my ( $dbh, $to_biblio, $from_biblio ) = @_; + my $sth = $dbh->prepare( + "SELECT count(*) as reservenumber FROM reserves WHERE biblionumber = ?" + ); + $sth->execute($from_biblio); + if ( my $data = $sth->fetchrow_hashref() ) { + + # holds exist on old record, if not we don't need to do anything + $sth = $dbh->prepare( + "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?"); + $sth->execute( $to_biblio, $from_biblio ); + + # Reorder by date + # don't reorder those already waiting + + $sth = $dbh->prepare( +"SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC" + ); + my $upd_sth = $dbh->prepare( +"UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ? + AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) " + ); + $sth->execute( $to_biblio, 'W', 'T' ); + my $priority = 1; + while ( my $reserve = $sth->fetchrow_hashref() ) { + $upd_sth->execute( + $priority, $to_biblio, + $reserve->{'borrowernumber'}, $reserve->{'reservedate'}, + $reserve->{'constrainttype'}, $reserve->{'itemnumber'} + ); + $priority++; + } + } +} + +=head2 ReserveSlip + + ReserveSlip($branchcode, $borrowernumber, $biblionumber) + + Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef + +=cut + +sub ReserveSlip { + my ($branch, $borrowernumber, $biblionumber) = @_; + +# return unless ( C4::Context->boolean_preference('printreserveslips') ); + + my $reserve = GetReserveInfo($borrowernumber,$biblionumber ) + or return; + + return C4::Letters::GetPreparedLetter ( + module => 'circulation', + letter_code => 'RESERVESLIP', + branchcode => $branch, + tables => { + 'reserves' => $reserve, + 'branches' => $reserve->{branchcode}, + 'borrowers' => $reserve, + 'biblio' => $reserve, + 'items' => $reserve, + }, + ); +} =head1 AUTHOR -Koha Developement team +Koha Development Team =cut