X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=C4%2FReserves.pm;h=9c760b6e2829aaa6615f64f0706ae2d5e34f5dda;hb=1970b245f5426506a0eed92d74f530de2e909d37;hp=1dc6f72ab8f894facd44e0e746dacbdb12859577;hpb=ee3eee451d2c36ff58f9443c86a8522ef905eca1;p=koha.git diff --git a/C4/Reserves.pm b/C4/Reserves.pm index 1dc6f72ab8..9c760b6e28 100644 --- a/C4/Reserves.pm +++ b/C4/Reserves.pm @@ -7,18 +7,18 @@ package C4::Reserves; # # This file is part of Koha. # -# Koha is free software; you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. # -# Koha is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . use strict; @@ -38,8 +38,10 @@ use C4::Branch qw( GetBranchDetail ); use C4::Dates qw( format_date_in_iso ); use Koha::DateUtils; +use Koha::Calendar; +use Koha::Database; -use List::MoreUtils qw( firstidx ); +use List::MoreUtils qw( firstidx any ); use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); @@ -68,13 +70,13 @@ This modules provides somes functions to deal with reservations. The complete workflow is : ==== 1st use case ==== patron request a document, 1st available : P >0, F=NULL, I=NULL - a library having it run "transfertodo", and clic on the list + 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 + patron can pick it up P =0, F=W, 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 @@ -93,7 +95,8 @@ BEGIN { @ISA = qw(Exporter); @EXPORT = qw( &AddReserve - + + &GetReserve &GetReservesFromItemnumber &GetReservesFromBiblionumber &GetReservesFromBorrowernumber @@ -103,9 +106,9 @@ BEGIN { &GetReserveFee &GetReserveInfo &GetReserveStatus - + &GetOtherReserves - + &ModReserveFill &ModReserveAffect &ModReserve @@ -113,26 +116,33 @@ BEGIN { &ModReserveCancelAll &ModReserveMinusPriority &MoveReserve - + &CheckReserves &CanBookBeReserved - &CanItemBeReserved + &CanItemBeReserved + &CanReserveBeCanceledFromOpac &CancelReserve &CancelExpiredReserves &AutoUnsuspendReserves &IsAvailableForItemLevelRequest - + + &OPACItemHoldsAllowed + &AlterPriority &ToggleLowestPriority &ReserveSlip &ToggleSuspend &SuspendAll + + &GetReservesControlBranch + + IsItemOnHoldAndFound ); @EXPORT_OK = qw( MergeHolds ); -} +} =head2 AddReserve @@ -173,32 +183,33 @@ sub AddReserve { # updates take place here if ( $fee > 0 ) { my $nextacctno = &getnextacctno( $borrowernumber ); - my $query = qq/ + my $query = qq{ INSERT INTO accountlines (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding) VALUES (?,?,now(),?,?,'Res',?) - /; + }; my $usth = $dbh->prepare($query); $usth->execute( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ); } #if ($const eq 'a'){ - my $query = qq/ + my $query = qq{ INSERT INTO reserves (borrowernumber,biblionumber,reservedate,branchcode,constrainttype, priority,reservenotes,itemnumber,found,waitingdate,expirationdate) VALUES (?,?,?,?,?, ?,?,?,?,?,?) - /; + }; my $sth = $dbh->prepare($query); $sth->execute( $borrowernumber, $biblionumber, $resdate, $branch, $const, $priority, $notes, $checkitem, $found, $waitingdate, $expdate ); + my $reserve_id = $sth->{mysql_insertid}; # Send e-mail to librarian if syspref is active if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){ @@ -212,6 +223,7 @@ sub AddReserve { 'branches' => $branch_details, 'borrowers' => $borrower, 'biblio' => $biblionumber, + 'items' => $checkitem, }, ) ) { @@ -230,37 +242,71 @@ sub AddReserve { #} ($const eq "o" || $const eq "e") or return; # FIXME: why not have a useful return value? - $query = qq/ + $query = qq{ INSERT INTO reserveconstraints (borrowernumber,biblionumber,reservedate,biblioitemnumber) VALUES (?,?,?,?) - /; + }; $sth = $dbh->prepare($query); # keep prepare outside the loop! foreach (@$bibitems) { $sth->execute($borrowernumber, $biblionumber, $resdate, $_); } - return; # FIXME: why not have a useful return value? + return $reserve_id; +} + +=head2 GetReserve + + $res = GetReserve( $reserve_id ); + + Return the current reserve. + +=cut + +sub GetReserve { + my ($reserve_id) = @_; + + my $dbh = C4::Context->dbh; + my $query = "SELECT * FROM reserves WHERE reserve_id = ?"; + my $sth = $dbh->prepare( $query ); + $sth->execute( $reserve_id ); + return $sth->fetchrow_hashref(); } =head2 GetReservesFromBiblionumber - ($count, $title_reserves) = &GetReserves($biblionumber); + my $reserves = GetReservesFromBiblionumber({ + biblionumber => $biblionumber, + [ itemnumber => $itemnumber, ] + [ all_dates => 1|0 ] + }); + +This function gets the list of reservations for one C<$biblionumber>, +returning an arrayref pointing to the reserves for C<$biblionumber>. + +By default, only reserves whose start date falls before the current +time are returned. To return all reserves, including future ones, +the C parameter can be included and set to a true value. -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>. +If the C parameter is supplied, reserves must be targeted +to that item or not targeted to any item at all; otherwise, they +are excluded from the list. =cut sub GetReservesFromBiblionumber { - my ($biblionumber) = shift or return (0, []); - my ($all_dates) = shift; + my ( $params ) = @_; + my $biblionumber = $params->{biblionumber} or return []; + my $itemnumber = $params->{itemnumber}; + my $all_dates = $params->{all_dates} // 0; my $dbh = C4::Context->dbh; # Find the desired items in the reserves + my @params; my $query = " - SELECT branchcode, + SELECT reserve_id, + branchcode, timestamp AS rtimestamp, priority, biblionumber, @@ -276,12 +322,17 @@ sub GetReservesFromBiblionumber { suspend_until FROM reserves WHERE biblionumber = ? "; + push( @params, $biblionumber ); unless ( $all_dates ) { - $query .= "AND reservedate <= CURRENT_DATE()"; + $query .= " AND reservedate <= CAST(NOW() AS DATE) "; + } + if ( $itemnumber ) { + $query .= " AND ( itemnumber IS NULL OR itemnumber = ? )"; + push( @params, $itemnumber ); } $query .= "ORDER BY priority"; my $sth = $dbh->prepare($query); - $sth->execute($biblionumber); + $sth->execute( @params ); my @results; my $i = 0; while ( my $data = $sth->fetchrow_hashref ) { @@ -302,7 +353,7 @@ sub GetReservesFromBiblionumber { push( @bibitemno, $bibitemnos ); # FIXME: inefficient: use fetchall_arrayref } my $count = scalar @bibitemno; - + # if we have two or more different specific itemtypes # reserved by same person on same day my $bdata; @@ -323,32 +374,47 @@ sub GetReservesFromBiblionumber { } push @results, $data; } - return ( $#results + 1, \@results ); + return \@results; } =head2 GetReservesFromItemnumber - ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber); + ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $waitingdate ) = GetReservesFromItemnumber($itemnumber); -TODO :: Description here +Get the first reserve for a specific item number (based on priority). Returns the abovementioned values for that reserve. + +The routine does not look at future reserves (read: item level holds), but DOES include future waits (a confirmed future hold). =cut sub GetReservesFromItemnumber { - 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 ); + my ($itemnumber) = @_; + + my $schema = Koha::Database->new()->schema(); + + my $r = $schema->resultset('Reserve')->search( + { + itemnumber => $itemnumber, + suspend => 0, + -or => [ + reservedate => \'<= CAST( NOW() AS DATE )', + waitingdate => { '!=', undef } + ] + }, + { + order_by => 'priority', + } + )->first(); + + return unless $r; + + return ( + $r->reservedate(), + $r->get_column('borrowernumber'), + $r->get_column('branchcode'), + $r->reserve_id(), + $r->waitingdate(), + ); } =head2 GetReservesFromBorrowernumber @@ -387,7 +453,10 @@ sub GetReservesFromBorrowernumber { #------------------------------------------------------------------------------------- =head2 CanBookBeReserved - $error = &CanBookBeReserved($borrowernumber, $biblionumber) + $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber) + if ($canReserve eq 'OK') { #We can reserve this Item! } + +See CanItemBeReserved() for possible return values. =cut @@ -401,46 +470,64 @@ sub CanBookBeReserved{ push (@$items,@hostitems); } - foreach my $item (@$items){ - return 1 if CanItemBeReserved($borrowernumber, $item); + my $canReserve; + foreach my $item (@$items) { + $canReserve = CanItemBeReserved( $borrowernumber, $item ); + return 'OK' if $canReserve eq 'OK'; } - return 0; + return $canReserve; } =head2 CanItemBeReserved - $error = &CanItemBeReserved($borrowernumber, $itemnumber) + $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber) + if ($canReserve eq 'OK') { #We can reserve this Item! } -This function return 1 if an item can be issued by this borrower. +@RETURNS OK, if the Item can be reserved. + ageRestricted, if the Item is age restricted for this borrower. + damaged, if the Item is damaged. + cannotReserveFromOtherBranches, if syspref 'canreservefromotherbranches' is OK. + tooManyReserves, if the borrower has exceeded his maximum reserve amount. + notReservable, if holds on this item are not allowed =cut sub CanItemBeReserved{ my ($borrowernumber, $itemnumber) = @_; - + my $dbh = C4::Context->dbh; + my $ruleitemtype; # itemtype of the matching issuing rule my $allowedreserves = 0; + # we retrieve borrowers and items informations # + # item->{itype} will come for biblioitems if necessery + my $item = GetItem($itemnumber); + my $biblioData = C4::Biblio::GetBiblioData( $item->{biblionumber} ); + my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber); + + # If an item is damaged and we don't allow holds on damaged items, we can stop right here + return 'damaged' if ( $item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems') ); + + #Check for the age restriction + my ($ageRestriction, $daysToAgeRestriction) = C4::Circulation::GetAgeRestriction( $biblioData->{agerestriction}, $borrower ); + return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0; + my $controlbranch = C4::Context->preference('ReservesControlBranch'); - my $itype = C4::Context->preference('item-level_itypes') ? "itype" : "itemtype"; + my $itemtypefield = 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, + 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 + + my $querycount ="SELECT count(*) as count FROM reserves LEFT JOIN items USING (itemnumber) @@ -450,11 +537,9 @@ sub CanItemBeReserved{ "; - my $itemtype = $item->{$itype}; - my $categorycode = $borrower->{categorycode}; my $branchcode = ""; my $branchfield = "reserves.branchcode"; - + if( $controlbranch eq "ItemHomeLibrary" ){ $branchfield = "items.homebranch"; $branchcode = $item->{homebranch}; @@ -464,39 +549,88 @@ sub CanItemBeReserved{ } # we retrieve rights - $sth->execute($categorycode, $itemtype, $branchcode); + $sth->execute($borrower->{'categorycode'}, $item->{'itype'}, $branchcode); if(my $rights = $sth->fetchrow_hashref()){ - $itemtype = $rights->{itemtype}; + $ruleitemtype = $rights->{itemtype}; $allowedreserves = $rights->{reservesallowed}; }else{ - $itemtype = '*'; + $ruleitemtype = '*'; } - + # we retrieve count - + $querycount .= "AND $branchfield = ?"; - $querycount .= " AND $itype = ?" if ($itemtype ne "*"); + $querycount .= " AND $itemtypefield = ?" if ($ruleitemtype ne "*"); my $sthcount = $dbh->prepare($querycount); - if($itemtype eq "*"){ + if($ruleitemtype eq "*"){ $sthcount->execute($borrowernumber, $branchcode); }else{ - $sthcount->execute($borrowernumber, $branchcode, $itemtype); + $sthcount->execute($borrowernumber, $branchcode, $ruleitemtype); } - + 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; + if( $reservecount >= $allowedreserves ){ + return 'tooManyReserves'; } + + my $circ_control_branch = C4::Circulation::_GetCircControlBranch($item, + $borrower); + my $branchitemrule = C4::Circulation::GetBranchItemRule($circ_control_branch, + $item->{itype}); + + if ( $branchitemrule->{holdallowed} == 0 ) { + return 'notReservable'; + } + + if ( $branchitemrule->{holdallowed} == 1 + && $borrower->{branchcode} ne $item->{homebranch} ) + { + return 'cannotReserveFromOtherBranches'; + } + + # If reservecount is ok, we check item branch if IndependentBranches is ON + # and canreservefromotherbranches is OFF + if ( C4::Context->preference('IndependentBranches') + and !C4::Context->preference('canreservefromotherbranches') ) + { + my $itembranch = $item->{homebranch}; + if ($itembranch ne $borrower->{branchcode}) { + return 'cannotReserveFromOtherBranches'; + } + } + + return 'OK'; +} + +=head2 CanReserveBeCanceledFromOpac + + $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber); + + returns 1 if reserve can be cancelled by user from OPAC. + First check if reserve belongs to user, next checks if reserve is not in + transfer or waiting status + +=cut + +sub CanReserveBeCanceledFromOpac { + my ($reserve_id, $borrowernumber) = @_; + + return unless $reserve_id and $borrowernumber; + my $reserve = GetReserve($reserve_id); + + return 0 unless $reserve->{borrowernumber} == $borrowernumber; + return 0 if ( $reserve->{found} eq 'W' ) or ( $reserve->{found} eq 'T' ); + + return 1; + } + #-------------------------------------------------------------------------------- =head2 GetReserveCount @@ -511,11 +645,11 @@ sub GetReserveCount { my $dbh = C4::Context->dbh; - my $query = ' + my $query = " SELECT COUNT(*) AS counter FROM reserves - WHERE borrowernumber = ? - '; + WHERE borrowernumber = ? + "; my $sth = $dbh->prepare($query); $sth->execute($borrowernumber); my $row = $sth->fetchrow_hashref; @@ -542,8 +676,7 @@ sub GetOtherReserves { #minus priorities of others reservs ModReserveMinusPriority( $itemnumber, - $checkreserves->{'borrowernumber'}, - $iteminfo->{'biblionumber'} + $checkreserves->{'reserve_id'}, ); #launch the subroutine dotransfer @@ -560,8 +693,7 @@ sub GetOtherReserves { $messages->{'waiting'} = 1; ModReserveMinusPriority( $itemnumber, - $checkreserves->{'borrowernumber'}, - $iteminfo->{'biblionumber'} + $checkreserves->{'reserve_id'}, ); ModReserveStatus($itemnumber,'W'); } @@ -586,15 +718,14 @@ sub GetReserveFee { #check for issues; my $dbh = C4::Context->dbh; my $const = lc substr( $constraint, 0, 1 ); - my $query = qq/ + my $query = qq{ SELECT * FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ? - /; + }; my $sth = $dbh->prepare($query); $sth->execute($borrowernumber); my $data = $sth->fetchrow_hashref; - $sth->finish(); my $fee = $data->{'reservefee'}; my $cntitems = @- > $bibitems; @@ -635,7 +766,6 @@ sub GetReserveFee { } } } - $sth1->finish; my $cntitemsfound = @biblioitems; my $issues = 0; my $x = 0; @@ -687,7 +817,7 @@ sub GetReservesToBranch { my ( $frombranch ) = @_; my $dbh = C4::Context->dbh; my $sth = $dbh->prepare( - "SELECT borrowernumber,reservedate,itemnumber,timestamp + "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp FROM reserves WHERE priority='0' AND branchcode=?" @@ -710,22 +840,24 @@ sub GetReservesToBranch { sub GetReservesForBranch { my ($frombranch) = @_; - my $dbh = C4::Context->dbh; - my $query = "SELECT borrowernumber,reservedate,itemnumber,waitingdate + my $dbh = C4::Context->dbh; + + my $query = " + SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate FROM reserves WHERE priority='0' - AND found='W' "; - if ($frombranch){ - $query .= " AND branchcode=? "; - } + AND found='W' + "; + $query .= " AND branchcode=? " if ( $frombranch ); $query .= "ORDER BY waitingdate" ; + my $sth = $dbh->prepare($query); if ($frombranch){ - $sth->execute($frombranch); - } - else { - $sth->execute(); - } + $sth->execute($frombranch); + } else { + $sth->execute(); + } + my @transreserv; my $i = 0; while ( my $data = $sth->fetchrow_hashref ) { @@ -735,26 +867,51 @@ sub GetReservesForBranch { return (@transreserv); } +=head2 GetReserveStatus + + $reservestatus = GetReserveStatus($itemnumber); + +Takes an itemnumber and returns the status of the reserve placed on it. +If several reserves exist, the reserve with the lower priority is given. + +=cut + +## FIXME: I don't think this does what it thinks it does. +## It only ever checks the first reserve result, even though +## multiple reserves for that bib can have the itemnumber set +## the sub is only used once in the codebase. 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; + + my ($sth, $found, $priority); + if ( $itemnumber ) { + $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1"); + $sth->execute($itemnumber); + ($found, $priority) = $sth->fetchrow_array; + } + + if(defined $found) { + return 'Waiting' if $found eq 'W' and $priority == 0; + return 'Finished' if $found eq 'F'; + } + + return 'Reserved' if $priority > 0; + + return ''; # empty string here will remove need for checking undef, or less log lines } =head2 CheckReserves ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber); ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode); + ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead); Find a book in the reserves. C<$itemnumber> is the book's item number. +C<$lookahead> is the number of days to look in advance for future reserves. As I understand it, C<&CheckReserves> looks for the given item in the reserves. If it is found, that's a match, and C<$status> is set to @@ -775,7 +932,7 @@ table in the Koha database. =cut sub CheckReserves { - my ( $item, $barcode ) = @_; + my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_; my $dbh = C4::Context->dbh; my $sth; my $select; @@ -785,7 +942,10 @@ sub CheckReserves { items.biblioitemnumber, itemtypes.notforloan, items.notforloan AS itemnotforloan, - items.itemnumber + items.itemnumber, + items.damaged, + items.homebranch, + items.holdingbranch FROM items LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype @@ -797,13 +957,16 @@ sub CheckReserves { items.biblioitemnumber, itemtypes.notforloan, items.notforloan AS itemnotforloan, - items.itemnumber + items.itemnumber, + items.damaged, + items.homebranch, + items.holdingbranch FROM items LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype "; } - + if ($item) { $sth = $dbh->prepare("$select WHERE itemnumber = ?"); $sth->execute($item); @@ -813,16 +976,18 @@ sub CheckReserves { $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; + my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array; + + return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') ); - return ( '' ) unless $itemnumber; # bail if we got nothing. + return unless $itemnumber; # bail if we got nothing. # if item is not for loan it cannot be reserved either..... - # execpt where items.notforloan < 0 : This indicates the item is holdable. - return ( '' ) if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype; + # except where items.notforloan < 0 : This indicates the item is holdable. + return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype; # Find this item in the reserves - my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber ); + my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers); # $priority and $highest are used to find the most important item # in the list returned by &_Findgroupreserve. (The lower $priority, @@ -830,21 +995,47 @@ sub CheckReserves { # $highest is the most important item we've seen so far. my $highest; if (scalar @reserves) { + my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority'); + my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl'); + my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl'); + my $priority = 10000000; foreach my $res (@reserves) { if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) { return ( "Waiting", $res, \@reserves ); # Found it } else { + my $borrowerinfo; + my $iteminfo; + my $local_hold_match; + + if ($LocalHoldsPriority) { + $borrowerinfo = C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} ); + $iteminfo = C4::Items::GetItem($itemnumber); + + my $local_holds_priority_item_branchcode = + $iteminfo->{$LocalHoldsPriorityItemControl}; + my $local_holds_priority_patron_branchcode = + ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' ) + ? $res->{branchcode} + : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' ) + ? $borrowerinfo->{branchcode} + : undef; + $local_hold_match = + $local_holds_priority_item_branchcode eq + $local_holds_priority_patron_branchcode; + } + # 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); + if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) { + $borrowerinfo ||= C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} ); + $iteminfo ||= C4::Items::GetItem($itemnumber); + my $branch = GetReservesControlBranch( $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; + last if $local_hold_match; } } } @@ -873,31 +1064,46 @@ 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() ) + 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'} ); + CancelReserve({ reserve_id => $res->{'reserve_id'} }); } - + # 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 $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays'); + + my $today = dt_from_string(); 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); + while ( my $res = $sth->fetchrow_hashref ) { + my $do_cancel = 1; + unless ( $cancel_on_holidays ) { + my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} ); + my $is_holiday = $calendar->is_holiday( $today ); + + if ( $is_holiday ) { + $do_cancel = 0; + } } - CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} ); + if ( $do_cancel ) { + if ( $charge ) { + manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge); + } + + CancelReserve({ reserve_id => $res->{'reserve_id'} }); + } } } @@ -923,109 +1129,64 @@ sub AutoUnsuspendReserves { =head2 CancelReserve - &CancelReserve($biblionumber, $itemnumber, $borrowernumber); + CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber ] }); 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> uses -C<$itemnumber>. +=cut -C<$borrowernumber> is the borrower number of the patron on whose -behalf the book was reserved. +sub CancelReserve { + my ( $params ) = @_; -If C<$biblionumber> was given, C<&CancelReserve> also adjusts the -priorities of the other people who are waiting on the book. + my $reserve_id = $params->{'reserve_id'}; + $reserve_id = GetReserveId( $params ) unless ( $reserve_id ); -=cut + return unless ( $reserve_id ); -sub CancelReserve { - my ( $biblio, $item, $borr ) = @_; my $dbh = C4::Context->dbh; - if ( $item and $borr ) { - # removing a waiting reserve record.... - # update the database... + + my $reserve = GetReserve( $reserve_id ); + if ($reserve) { my $query = " UPDATE reserves SET cancellationdate = now(), found = Null, priority = 0 - WHERE itemnumber = ? - AND borrowernumber = ? + WHERE reserve_id = ? "; my $sth = $dbh->prepare($query); - $sth->execute( $item, $borr ); - $sth->finish; + $sth->execute( $reserve_id ); + $query = " INSERT INTO old_reserves SELECT * FROM reserves - WHERE itemnumber = ? - AND borrowernumber = ? + WHERE reserve_id = ? "; $sth = $dbh->prepare($query); - $sth->execute( $item, $borr ); + $sth->execute( $reserve_id ); + $query = " DELETE FROM reserves - WHERE itemnumber = ? - AND borrowernumber = ? + WHERE reserve_id = ? "; $sth = $dbh->prepare($query); - $sth->execute( $item, $borr ); - } - else { - # removing a reserve record.... - # get the prioritiy on this record.... - my $priority; - my $query = qq/ - SELECT priority FROM reserves - WHERE biblionumber = ? - AND borrowernumber = ? - AND cancellationdate IS NULL - AND itemnumber IS NULL - /; - my $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borr ); - ($priority) = $sth->fetchrow_array; - $sth->finish; - $query = qq/ - UPDATE reserves - SET cancellationdate = now(), - found = Null, - priority = 0 - WHERE biblionumber = ? - AND borrowernumber = ? - /; - - # update the database, removing the record... - $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borr ); - $sth->finish; - - $query = qq/ - INSERT INTO old_reserves - SELECT * FROM reserves - WHERE biblionumber = ? - AND borrowernumber = ? - /; - $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borr ); - - $query = qq/ - DELETE FROM reserves - WHERE biblionumber = ? - AND borrowernumber = ? - /; - $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borr ); + $sth->execute( $reserve_id ); # now fix the priority on the others.... - _FixPriority( $biblio, $borr ); + _FixPriority({ biblionumber => $reserve->{biblionumber} }); } + + return $reserve; } =head2 ModReserve - ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber]) + ModReserve({ rank => $rank, + reserve_id => $reserve_id, + branchcode => $branchcode + [, itemnumber => $itemnumber ] + [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ] + }); Change a hold request's priority or cancel it. @@ -1038,12 +1199,12 @@ If C<$rank> is 'del', the hold request is cancelled. 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 +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. +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 +C<$rank> is a non-zero integer; if supplied, the itemnumber of the hold request is set accordingly; if omitted, the itemnumber is cleared. @@ -1055,59 +1216,44 @@ itemnumber and supplying itemnumber. =cut sub ModReserve { - #subroutine to update a reserve - my ( $rank, $biblio, $borrower, $branch , $itemnumber, $suspend_until) = @_; - return if $rank eq "W"; - return if $rank eq "n"; + my ( $params ) = @_; + + my $rank = $params->{'rank'}; + my $reserve_id = $params->{'reserve_id'}; + my $branchcode = $params->{'branchcode'}; + my $itemnumber = $params->{'itemnumber'}; + my $suspend_until = $params->{'suspend_until'}; + my $borrowernumber = $params->{'borrowernumber'}; + my $biblionumber = $params->{'biblionumber'}; + + return if $rank eq "W"; + return if $rank eq "n"; + + return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) ); + $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id ); + my $dbh = C4::Context->dbh; if ( $rank eq "del" ) { - my $query = qq/ - UPDATE reserves - SET cancellationdate=now() - WHERE biblionumber = ? - AND borrowernumber = ? - /; - my $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borrower ); - $sth->finish; - $query = qq/ - INSERT INTO old_reserves - SELECT * - FROM reserves - WHERE biblionumber = ? - AND borrowernumber = ? - /; - $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borrower ); - $query = qq/ - DELETE FROM reserves - WHERE biblionumber = ? - AND borrowernumber = ? - /; - $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borrower ); - + CancelReserve({ reserve_id => $reserve_id }); } elsif ($rank =~ /^\d+/ and $rank > 0) { my $query = " UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL - WHERE biblionumber = ? - AND borrowernumber = ? + WHERE reserve_id = ? "; my $sth = $dbh->prepare($query); - $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower); - $sth->finish; + $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id ); if ( defined( $suspend_until ) ) { if ( $suspend_until ) { $suspend_until = C4::Dates->new( $suspend_until )->output("iso"); - $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $suspend_until, $biblio, $borrower ) ); + $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) ); } else { - $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $biblio, $borrower ) ); + $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) ); } } - _FixPriority( $biblio, $borrower, $rank); + _FixPriority({ reserve_id => $reserve_id, rank =>$rank }); } } @@ -1127,6 +1273,7 @@ sub ModReserveFill { my ($res) = @_; my $dbh = C4::Context->dbh; # fill in a reserve record.... + my $reserve_id = $res->{'reserve_id'}; my $biblionumber = $res->{'biblionumber'}; my $borrowernumber = $res->{'borrowernumber'}; my $resdate = $res->{'reservedate'}; @@ -1141,7 +1288,6 @@ sub ModReserveFill { my $sth = $dbh->prepare($query); $sth->execute( $biblionumber, $borrowernumber, $resdate ); ($priority) = $sth->fetchrow_array; - $sth->finish; # update the database... $query = "UPDATE reserves @@ -1153,7 +1299,6 @@ sub ModReserveFill { "; $sth = $dbh->prepare($query); $sth->execute( $biblionumber, $resdate, $borrowernumber ); - $sth->finish; # move to old_reserves $query = "INSERT INTO old_reserves @@ -1171,11 +1316,11 @@ sub ModReserveFill { "; $sth = $dbh->prepare($query); $sth->execute( $biblionumber, $resdate, $borrowernumber ); - + # now fix the priority on the others (if the priority wasn't # already sorted!).... unless ( $priority == 0 ) { - _FixPriority( $biblionumber, $borrowernumber ); + _FixPriority({ reserve_id => $reserve_id, biblionumber => $biblionumber }); } } @@ -1216,7 +1361,7 @@ with the biblionumber & the borrowernumber, we can affect the itemnumber 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 +otherwise, a transfer is on the way, and the end of the transfer will take care of the waiting status =cut @@ -1233,7 +1378,12 @@ sub ModReserveAffect { # 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 $reserve_id = GetReserveId({ + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + }); + return unless defined $reserve_id; + my $request = GetReserveInfo($reserve_id); 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 @@ -1263,7 +1413,7 @@ sub ModReserveAffect { $sth = $dbh->prepare($query); $sth->execute( $itemnumber, $borrowernumber,$biblionumber); _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf ); - + _FixPriority( { biblionumber => $biblionumber } ); if ( C4::Context->preference("ReturnToShelvingCart") ) { CartToShelf( $itemnumber ); } @@ -1285,7 +1435,7 @@ sub ModReserveCancelAll { my ( $itemnumber, $borrowernumber ) = @_; #step 1 : cancel the reservation - my $CancelReserve = CancelReserve( undef, $itemnumber, $borrowernumber ); + my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber }); #step 2 launch the subroutine of the others reserves ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber); @@ -1297,30 +1447,29 @@ sub ModReserveCancelAll { &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber) -Reduce the values of queuded list +Reduce the values of queued list =cut sub ModReserveMinusPriority { - my ( $itemnumber, $borrowernumber, $biblionumber ) = @_; + my ( $itemnumber, $reserve_id ) = @_; #first step update the value of the first person on reserv my $dbh = C4::Context->dbh; my $query = " UPDATE reserves SET priority = 0 , itemnumber = ? - WHERE borrowernumber=? - AND biblionumber=? + WHERE reserve_id = ? "; my $sth_upd = $dbh->prepare($query); - $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber ); - # second step update all others reservs - _FixPriority($biblionumber, $borrowernumber, '0'); + $sth_upd->execute( $itemnumber, $reserve_id ); + # second step update all others reserves + _FixPriority({ reserve_id => $reserve_id, rank => '0' }); } =head2 GetReserveInfo - &GetReserveInfo($borrowernumber,$biblionumber); + &GetReserveInfo($reserve_id); Get item and borrower details for a current hold. Current implementation this query should have a single result. @@ -1328,126 +1477,152 @@ Current implementation this query should have a single result. =cut sub GetReserveInfo { - my ( $borrowernumber, $biblionumber ) = @_; + my ( $reserve_id ) = @_; my $dbh = C4::Context->dbh; - 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=? - AND reserves.biblionumber=?"; - my $sth = $dbh->prepare($strsth); - $sth->execute($borrowernumber,$biblionumber); - - my $data = $sth->fetchrow_hashref; - return $data; + my $strsth="SELECT + reserve_id, + 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.reserve_id = ?"; + my $sth = $dbh->prepare($strsth); + $sth->execute($reserve_id); + my $data = $sth->fetchrow_hashref; + return $data; } =head2 IsAvailableForItemLevelRequest - my $is_available = IsAvailableForItemLevelRequest($itemnumber); + my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record); 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 +* 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. +Need to check the issuingrules onshelfholds column, +if this is set items on the shelf can be placed on hold 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 +this routine does not check IndependentBranches and canreservefromotherbranches. =cut sub IsAvailableForItemLevelRequest { - my $itemnumber = shift; - - my $item = GetItem($itemnumber); + my $item = shift; + my $borrower = shift; + my $dbh = C4::Context->dbh; # 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; + my $itype = _get_itype($item); + my $notforloan_per_itemtype + = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?", + undef, $itype); + + return 0 if + $notforloan_per_itemtype || + $item->{itemlost} || + $item->{notforloan} > 0 || + $item->{withdrawn} || + ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems')); + + + return 1 if _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch}); + + return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting"; +} + +=head2 OnShelfHoldsAllowed + + OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode); + +Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf +holds are allowed, returns true if so. + +=cut + +sub OnShelfHoldsAllowed { + my ($item, $borrower) = @_; + + my $itype = _get_itype($item); + return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch}); +} + +sub _get_itype { + my $item = shift; + + my $itype; 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 = ?"; + # We cant trust GetItem to honour the syspref, so safest to do it ourselves + # When GetItem is fixed, we can remove this + $itype = $item->{itype}; } - 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; + else { + # XXX This is a bit dodgy. It relies on biblio itemtype column having different name. + # So if we already have a biblioitems join when calling this function, + # we don't need to access the database again + $itype = $item->{itemtype}; } + unless ($itype) { + my $dbh = C4::Context->dbh; + my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? "; + my $sth = $dbh->prepare($query); + $sth->execute($item->{biblioitemnumber}); + if (my $data = $sth->fetchrow_hashref()){ + $itype = $data->{itemtype}; + } + } + return $itype; +} - 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; - +sub _OnShelfHoldsAllowed { + my ($itype,$borrowercategory,$branchcode) = @_; - if (C4::Context->preference('AllowOnShelfHolds')) { - return $available_per_item; - } else { - return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "W")); - } + my $rule = C4::Circulation::GetIssuingRule($borrowercategory, $itype, $branchcode); + return $rule->{onshelfholds}; } =head2 AlterPriority - AlterPriority( $where, $borrowernumber, $biblionumber, $reservedate ); + AlterPriority( $where, $reserve_id ); 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 @@ -1455,29 +1630,30 @@ Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was =cut sub AlterPriority { - my ( $where, $borrowernumber, $biblionumber ) = @_; + my ( $where, $reserve_id ) = @_; 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(); + my $reserve = GetReserve( $reserve_id ); + + if ( $reserve->{cancellationdate} ) { + warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')'; + return; + } if ( $where eq 'up' || $where eq 'down' ) { - - my $priority = $reserve->{'priority'}; + + my $priority = $reserve->{'priority'}; $priority = $where eq 'up' ? $priority - 1 : $priority + 1; - _FixPriority( $biblionumber, $borrowernumber, $priority ) + _FixPriority({ reserve_id => $reserve_id, rank => $priority }) } elsif ( $where eq 'top' ) { - _FixPriority( $biblionumber, $borrowernumber, '1' ) + _FixPriority({ reserve_id => $reserve_id, rank => '1' }) } elsif ( $where eq 'bottom' ) { - _FixPriority( $biblionumber, $borrowernumber, '999999' ) + _FixPriority({ reserve_id => $reserve_id, rank => '999999' }); } } @@ -1491,27 +1667,19 @@ This function sets the lowestPriority field to true if is false, and false if it =cut sub ToggleLowestPriority { - my ( $borrowernumber, $biblionumber ) = @_; + my ( $reserve_id ) = @_; 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; + my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?"); + $sth->execute( $reserve_id ); - _FixPriority( $biblionumber, $borrowernumber, '999999' ); + _FixPriority({ reserve_id => $reserve_id, rank => '999999' }); } =head2 ToggleSuspend - ToggleSuspend( $borrowernumber, $biblionumber ); + ToggleSuspend( $reserve_id ); This function sets the suspend field to true if is false, and false if it is true. If the reserve is currently suspended with a suspend_until date, that date will @@ -1520,9 +1688,15 @@ be cleared when it is unsuspended. =cut sub ToggleSuspend { - my ( $borrowernumber, $biblionumber, $suspend_until ) = @_; + my ( $reserve_id, $suspend_until ) = @_; - $suspend_until = output_pref( dt_from_string( $suspend_until ), 'iso' ) if ( $suspend_until ); + $suspend_until = output_pref( + { + dt => dt_from_string($suspend_until), + dateformat => 'iso', + dateonly => 1 + } + ) if ($suspend_until); my $do_until = ( $suspend_until ) ? '?' : 'NULL'; @@ -1531,17 +1705,14 @@ sub ToggleSuspend { my $sth = $dbh->prepare( "UPDATE reserves SET suspend = NOT suspend, suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END - WHERE biblionumber = ? - AND borrowernumber = ? + WHERE reserve_id = ? "); my @params; push( @params, $suspend_until ) if ( $suspend_until ); - push( @params, $biblionumber ); - push( @params, $borrowernumber ); + push( @params, $reserve_id ); $sth->execute( @params ); - $sth->finish; } =head2 SuspendAll @@ -1597,61 +1768,86 @@ sub SuspendAll { $dbh = C4::Context->dbh; $sth = $dbh->prepare( $query ); $sth->execute( @query_params ); - $sth->finish; } =head2 _FixPriority - &_FixPriority($biblio,$borrowernumber,$rank,$ignoreSetLowestRank); + _FixPriority({ + reserve_id => $reserve_id, + [rank => $rank,] + [ignoreSetLowestRank => $ignoreSetLowestRank] + }); + + or + + _FixPriority({ biblionumber => $biblionumber}); + +This routine adjusts the priority of a hold request and holds +on the same bib. + +In the first form, where a reserve_id is passed, the priority of the +hold is set to supplied rank, and other holds for that bib are adjusted +accordingly. If the rank is "del", the hold is cancelled. If no rank +is supplied, all of the holds on that bib have their priority adjusted +as if the second form had been used. -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 +In the second form, where a biblionumber is passed, the holds on that +bib (that are not captured) are sorted in order of increasing priority, +then have reserves.priority set so that the first non-captured hold +has its priority set to 1, the second non-captured hold has its priority +set to 2, and so forth. -=cut +In both cases, holds that have the lowestPriority flag on are have their +priority adjusted to ensure that they remain at the end of the line. + +Note that the ignoreSetLowestRank parameter is meant to be used only +when _FixPriority calls itself. + +=cut sub _FixPriority { - my ( $biblio, $borrowernumber, $rank, $ignoreSetLowestRank ) = @_; + my ( $params ) = @_; + my $reserve_id = $params->{reserve_id}; + my $rank = $params->{rank} // ''; + my $ignoreSetLowestRank = $params->{ignoreSetLowestRank}; + my $biblionumber = $params->{biblionumber}; + my $dbh = C4::Context->dbh; - if ( $rank eq "del" ) { - CancelReserve( $biblio, undef, $borrowernumber ); - } - if ( $rank eq "W" || $rank eq "0" ) { + + unless ( $biblionumber ) { + my $res = GetReserve( $reserve_id ); + $biblionumber = $res->{biblionumber}; + } + + if ( $rank eq "del" ) { + CancelReserve({ reserve_id => $reserve_id }); + } + elsif ( $rank eq "W" || $rank eq "0" ) { # make sure priority for waiting or in-transit items is 0 - my $query = qq/ + my $query = " UPDATE reserves SET priority = 0 - WHERE biblionumber = ? - AND borrowernumber = ? - AND found IN ('W', 'T') - /; + WHERE reserve_id = ? + AND found IN ('W', 'T') + "; my $sth = $dbh->prepare($query); - $sth->execute( $biblio, $borrowernumber ); + $sth->execute( $reserve_id ); } my @priority; - my @reservedates; # get whats left -# FIXME adding a new security in returned elements for changing priority, -# now, we don't care anymore any reservations with itemnumber linked (suppose a waiting reserve) - # This is wrong a waiting reserve has W set - # The assumption that having an itemnumber set means waiting is wrong and should be corrected any place it occurs - my $query = qq/ - SELECT borrowernumber, reservedate, constrainttype + my $query = " + SELECT reserve_id, borrowernumber, reservedate, constrainttype FROM reserves WHERE biblionumber = ? - AND ((found <> 'W' AND found <> 'T') or found is NULL) + AND ((found <> 'W' AND found <> 'T') OR found IS NULL) ORDER BY priority ASC - /; + "; my $sth = $dbh->prepare($query); - $sth->execute($biblio); + $sth->execute( $biblionumber ); while ( my $line = $sth->fetchrow_hashref ) { - push( @reservedates, $line ); push( @priority, $line ); } @@ -1659,7 +1855,7 @@ sub _FixPriority { my $i; my $key = -1; # to allow for 0 to be a valid result for ( $i = 0 ; $i < @priority ; $i++ ) { - if ( $borrowernumber == $priority[$i]->{'borrowernumber'} ) { + if ( $reserve_id == $priority[$i]->{'reserve_id'} ) { $key = $i; # save the index last; } @@ -1675,40 +1871,40 @@ sub _FixPriority { # now fix the priority on those that are left.... $query = " - UPDATE reserves - SET priority = ? - WHERE biblionumber = ? - AND borrowernumber = ? - AND reservedate = ? - AND found IS NULL + UPDATE reserves + SET priority = ? + WHERE reserve_id = ? "; $sth = $dbh->prepare($query); for ( my $j = 0 ; $j < @priority ; $j++ ) { $sth->execute( - $j + 1, $biblio, - $priority[$j]->{'borrowernumber'}, - $priority[$j]->{'reservedate'} + $j + 1, + $priority[$j]->{'reserve_id'} ); - $sth->finish; } - $sth = $dbh->prepare( "SELECT borrowernumber FROM reserves WHERE lowestPriority = 1 ORDER BY priority" ); + $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" ); $sth->execute(); - + unless ( $ignoreSetLowestRank ) { while ( my $res = $sth->fetchrow_hashref() ) { - _FixPriority( $biblio, $res->{'borrowernumber'}, '999999', 1 ); + _FixPriority({ + reserve_id => $res->{'reserve_id'}, + rank => '999999', + ignoreSetLowestRank => 1 + }); } } } =head2 _Findgroupreserve - @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber); + @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers); 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. +Lookahead is the number of days to look in advance. TODO: add more explanation about reserve constraints @@ -1720,12 +1916,12 @@ C. =cut sub _Findgroupreserve { - my ( $bibitem, $biblio, $itemnumber ) = @_; + my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_; 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/ + my $item_level_target_query = qq{ SELECT reserves.biblionumber AS biblionumber, reserves.borrowernumber AS borrowernumber, reserves.reservedate AS reservedate, @@ -1736,7 +1932,8 @@ sub _Findgroupreserve { reserves.priority AS priority, reserves.timestamp AS timestamp, biblioitems.biblioitemnumber AS biblioitemnumber, - reserves.itemnumber AS itemnumber + reserves.itemnumber AS itemnumber, + reserves.reserve_id AS reserve_id FROM reserves JOIN biblioitems USING (biblionumber) JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber) @@ -1744,19 +1941,21 @@ sub _Findgroupreserve { AND priority > 0 AND item_level_request = 1 AND itemnumber = ? - AND reservedate <= CURRENT_DATE() + AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY) AND suspend = 0 - /; + ORDER BY priority + }; my $sth = $dbh->prepare($item_level_target_query); - $sth->execute($itemnumber); + $sth->execute($itemnumber, $lookahead||0); my @results; if ( my $data = $sth->fetchrow_hashref ) { - push( @results, $data ); + push( @results, $data ) + unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ; } return @results if @results; - + # check for title-level targetted match - my $title_level_target_query = qq/ + my $title_level_target_query = qq{ SELECT reserves.biblionumber AS biblionumber, reserves.borrowernumber AS borrowernumber, reserves.reservedate AS reservedate, @@ -1767,7 +1966,8 @@ sub _Findgroupreserve { reserves.priority AS priority, reserves.timestamp AS timestamp, biblioitems.biblioitemnumber AS biblioitemnumber, - reserves.itemnumber AS itemnumber + reserves.itemnumber AS itemnumber, + reserves.reserve_id AS reserve_id FROM reserves JOIN biblioitems USING (biblionumber) JOIN hold_fill_targets USING (biblionumber, borrowernumber) @@ -1775,18 +1975,20 @@ sub _Findgroupreserve { AND priority > 0 AND item_level_request = 0 AND hold_fill_targets.itemnumber = ? - AND reservedate <= CURRENT_DATE() + AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY) AND suspend = 0 - /; + ORDER BY priority + }; $sth = $dbh->prepare($title_level_target_query); - $sth->execute($itemnumber); + $sth->execute($itemnumber, $lookahead||0); @results = (); if ( my $data = $sth->fetchrow_hashref ) { - push( @results, $data ); + push( @results, $data ) + unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ; } return @results if @results; - my $query = qq/ + my $query = qq{ SELECT reserves.biblionumber AS biblionumber, reserves.borrowernumber AS borrowernumber, reserves.reservedate AS reservedate, @@ -1798,7 +2000,8 @@ sub _Findgroupreserve { reserves.priority AS priority, reserves.timestamp AS timestamp, reserveconstraints.biblioitemnumber AS biblioitemnumber, - reserves.itemnumber AS itemnumber + reserves.itemnumber AS itemnumber, + reserves.reserve_id AS reserve_id FROM reserves LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber WHERE reserves.biblionumber = ? @@ -1807,14 +2010,16 @@ sub _Findgroupreserve { AND reserves.reservedate = reserveconstraints.reservedate ) OR reserves.constrainttype='a' ) AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?) - AND reserves.reservedate <= CURRENT_DATE() + AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY) AND suspend = 0 - /; + ORDER BY priority + }; $sth = $dbh->prepare($query); - $sth->execute( $biblio, $bibitem, $itemnumber ); + $sth->execute( $biblio, $bibitem, $itemnumber, $lookahead||0); @results = (); while ( my $data = $sth->fetchrow_hashref ) { - push( @results, $data ); + push( @results, $data ) + unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ; } return @results; } @@ -1833,25 +2038,14 @@ sub _koha_notify_reserve { 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' } ); - } else { - $print_mode = 1; - } + my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber); + + my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { + borrowernumber => $borrowernumber, + message_name => 'Hold_Filled' + } ); my $sth = $dbh->prepare(" SELECT * @@ -1878,44 +2072,41 @@ sub _koha_notify_reserve { substitute => { today => C4::Dates->new()->output() }, ); - - if ( $print_mode ) { - $letter_params{ 'letter_code' } = 'HOLD_PRINT'; - my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module"; + my $notification_sent = 0; #Keeping track if a Hold_filled message is sent. If no message can be sent, then default to a print message. + my $send_notification = sub { + my ( $mtt, $letter_code ) = (@_); + return unless defined $letter_code; + $letter_params{letter_code} = $letter_code; + $letter_params{message_transport_type} = $mtt; + my $letter = C4::Letters::GetPreparedLetter ( %letter_params ); + unless ($letter) { + warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module"; + return; + } C4::Letters::EnqueueLetter( { letter => $letter, borrowernumber => $borrowernumber, - message_transport_type => 'print', + from_address => $admin_email_address, + message_transport_type => $mtt, } ); - - return; - } - - if ( $to_address && defined $messagingprefs->{transports}->{'email'} ) { - $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'email'}; - my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module"; + }; - C4::Letters::EnqueueLetter( - { letter => $letter, - borrowernumber => $borrowernumber, - message_transport_type => 'email', - from_address => $admin_email_address, - } + while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) { + next if ( + ( $mtt eq 'email' and not $to_address ) # No email address + or ( $mtt eq 'sms' and not $borrower->{smsalertnumber} ) # No SMS number + or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl ); - } - - if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports}->{'sms'} ) { - $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'sms'}; - my $letter = C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module"; - C4::Letters::EnqueueLetter( - { letter => $letter, - borrowernumber => $borrowernumber, - message_transport_type => 'sms', - } - ); + &$send_notification($mtt, $letter_code); + $notification_sent++; + } + #Making sure that a print notification is sent if no other transport types can be utilized. + if (! $notification_sent) { + &$send_notification('print', 'HOLD'); } + } =head2 _ShiftPriorityByDateAndPriority @@ -1967,6 +2158,54 @@ sub _ShiftPriorityByDateAndPriority { return $new_priority; # so the caller knows what priority they wind up receiving } +=head2 OPACItemHoldsAllowed + + OPACItemHoldsAllowed($item_record,$borrower_record); + +Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see +if specific item holds are allowed, returns true if so. + +=cut + +sub OPACItemHoldsAllowed { + my ($item,$borrower) = @_; + + my $branchcode = $item->{homebranch} or die "No homebranch"; + my $itype; + my $dbh = C4::Context->dbh; + if (C4::Context->preference('item-level_itypes')) { + # We cant trust GetItem to honour the syspref, so safest to do it ourselves + # When GetItem is fixed, we can remove this + $itype = $item->{itype}; + } + else { + my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? "; + my $sth = $dbh->prepare($query); + $sth->execute($item->{biblioitemnumber}); + if (my $data = $sth->fetchrow_hashref()){ + $itype = $data->{itemtype}; + } + } + + my $query = "SELECT opacitemholds,categorycode,itemtype,branchcode FROM issuingrules WHERE + (issuingrules.categorycode = ? OR issuingrules.categorycode = '*') + AND + (issuingrules.itemtype = ? OR issuingrules.itemtype = '*') + AND + (issuingrules.branchcode = ? OR issuingrules.branchcode = '*') + ORDER BY + issuingrules.categorycode desc, + issuingrules.itemtype desc, + issuingrules.branchcode desc + LIMIT 1"; + my $sth = $dbh->prepare($query); + $sth->execute($borrower->{categorycode},$itype,$branchcode); + my $data = $sth->fetchrow_hashref; + my $opacitemholds = uc substr ($data->{opacitemholds}, 0, 1); + return '' if $opacitemholds eq 'N'; + return $opacitemholds; +} + =head2 MoveReserve MoveReserve( $itemnumber, $borrowernumber, $cancelreserve ) @@ -2007,9 +2246,15 @@ sub MoveReserve { ModReserveFill($borr_res); } - if ($cancelreserve) { # cancel reserves on this item - CancelReserve(0, $res->{'itemnumber'}, $res->{'borrowernumber'}); - CancelReserve($res->{'biblionumber'}, 0, $res->{'borrowernumber'}); + if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1 + RevertWaitingStatus({ itemnumber => $itemnumber }); + } + elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item + CancelReserve({ + biblionumber => $res->{'biblionumber'}, + itemnumber => $res->{'itemnumber'}, + borrowernumber => $res->{'borrowernumber'} + }); } } } @@ -2025,7 +2270,7 @@ This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by sub MergeHolds { my ( $dbh, $to_biblio, $from_biblio ) = @_; my $sth = $dbh->prepare( - "SELECT count(*) as reservenumber FROM reserves WHERE biblionumber = ?" + "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?" ); $sth->execute($from_biblio); if ( my $data = $sth->fetchrow_hashref() ) { @@ -2058,6 +2303,102 @@ sub MergeHolds { } } +=head2 RevertWaitingStatus + + RevertWaitingStatus({ itemnumber => $itemnumber }); + + Reverts a 'waiting' hold back to a regular hold with a priority of 1. + + Caveat: Any waiting hold fixed with RevertWaitingStatus will be an + item level hold, even if it was only a bibliolevel hold to + begin with. This is because we can no longer know if a hold + was item-level or bib-level after a hold has been set to + waiting status. + +=cut + +sub RevertWaitingStatus { + my ( $params ) = @_; + my $itemnumber = $params->{'itemnumber'}; + + return unless ( $itemnumber ); + + my $dbh = C4::Context->dbh; + + ## Get the waiting reserve we want to revert + my $query = " + SELECT * FROM reserves + WHERE itemnumber = ? + AND found IS NOT NULL + "; + my $sth = $dbh->prepare( $query ); + $sth->execute( $itemnumber ); + my $reserve = $sth->fetchrow_hashref(); + + ## Increment the priority of all other non-waiting + ## reserves for this bib record + $query = " + UPDATE reserves + SET + priority = priority + 1 + WHERE + biblionumber = ? + AND + priority > 0 + "; + $sth = $dbh->prepare( $query ); + $sth->execute( $reserve->{'biblionumber'} ); + + ## Fix up the currently waiting reserve + $query = " + UPDATE reserves + SET + priority = 1, + found = NULL, + waitingdate = NULL + WHERE + reserve_id = ? + "; + $sth = $dbh->prepare( $query ); + $sth->execute( $reserve->{'reserve_id'} ); + _FixPriority( { biblionumber => $reserve->{biblionumber} } ); +} + +=head2 GetReserveId + + $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] }); + + Returnes the first reserve id that matches the given criteria + +=cut + +sub GetReserveId { + my ( $params ) = @_; + + return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} ); + + my $dbh = C4::Context->dbh(); + + my $sql = "SELECT reserve_id FROM reserves WHERE "; + + my @params; + my @limits; + foreach my $key ( keys %$params ) { + if ( defined( $params->{$key} ) ) { + push( @limits, "$key = ?" ); + push( @params, $params->{$key} ); + } + } + + $sql .= join( " AND ", @limits ); + + my $sth = $dbh->prepare( $sql ); + $sth->execute( @params ); + my $row = $sth->fetchrow_hashref(); + + return $row->{'reserve_id'}; +} + =head2 ReserveSlip ReserveSlip($branchcode, $borrowernumber, $biblionumber) @@ -2071,8 +2412,11 @@ sub ReserveSlip { # return unless ( C4::Context->boolean_preference('printreserveslips') ); - my $reserve = GetReserveInfo($borrowernumber,$biblionumber ) - or return; + my $reserve_id = GetReserveId({ + biblionumber => $biblionumber, + borrowernumber => $borrowernumber + }) or return; + my $reserve = GetReserveInfo($reserve_id) or return; return C4::Letters::GetPreparedLetter ( module => 'circulation', @@ -2088,6 +2432,101 @@ sub ReserveSlip { ); } +=head2 GetReservesControlBranch + + my $reserves_control_branch = GetReservesControlBranch($item, $borrower); + + Return the branchcode to be used to determine which reserves + policy applies to a transaction. + + C<$item> is a hashref for an item. Only 'homebranch' is used. + + C<$borrower> is a hashref to borrower. Only 'branchcode' is used. + +=cut + +sub GetReservesControlBranch { + my ( $item, $borrower ) = @_; + + my $reserves_control = C4::Context->preference('ReservesControlBranch'); + + my $branchcode = + ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'} + : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'} + : undef; + + return $branchcode; +} + +=head2 CalculatePriority + + my $p = CalculatePriority($biblionumber, $resdate); + +Calculate priority for a new reserve on biblionumber, placing it at +the end of the line of all holds whose start date falls before +the current system time and that are neither on the hold shelf +or in transit. + +The reserve date parameter is optional; if it is supplied, the +priority is based on the set of holds whose start date falls before +the parameter value. + +After calculation of this priority, it is recommended to call +_ShiftPriorityByDateAndPriority. Note that this is currently done in +AddReserves. + +=cut + +sub CalculatePriority { + my ( $biblionumber, $resdate ) = @_; + + my $sql = q{ + SELECT COUNT(*) FROM reserves + WHERE biblionumber = ? + AND priority > 0 + AND (found IS NULL OR found = '') + }; + #skip found==W or found==T (waiting or transit holds) + if( $resdate ) { + $sql.= ' AND ( reservedate <= ? )'; + } + else { + $sql.= ' AND ( reservedate < NOW() )'; + } + my $dbh = C4::Context->dbh(); + my @row = $dbh->selectrow_array( + $sql, + undef, + $resdate ? ($biblionumber, $resdate) : ($biblionumber) + ); + + return @row ? $row[0]+1 : 1; +} + +=head2 IsItemOnHoldAndFound + + my $bool = IsItemFoundHold( $itemnumber ); + + Returns true if the item is currently on hold + and that hold has a non-null found status ( W, T, etc. ) + +=cut + +sub IsItemOnHoldAndFound { + my ($itemnumber) = @_; + + my $rs = Koha::Database->new()->schema()->resultset('Reserve'); + + my $found = $rs->count( + { + itemnumber => $itemnumber, + found => { '!=' => undef } + } + ); + + return $found; +} + =head1 AUTHOR Koha Development Team