Bug 11297: Add support for custom PQF attributes for Z39.50 server searches.
[srvgit] / C4 / Reserves.pm
index 57fb686..00d921d 100644 (file)
@@ -21,8 +21,7 @@ package C4::Reserves;
 # along with Koha; if not, see <http://www.gnu.org/licenses>.
 
 
-use strict;
-#use warnings; FIXME - Bug 2505
+use Modern::Perl;
 
 use C4::Accounts;
 use C4::Biblio;
@@ -41,7 +40,6 @@ use Koha::Database;
 use Koha::DateUtils;
 use Koha::Hold;
 use Koha::Holds;
-use Koha::IssuingRules;
 use Koha::ItemTypes;
 use Koha::Items;
 use Koha::Libraries;
@@ -143,7 +141,21 @@ BEGIN {
 
 =head2 AddReserve
 
-    AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
+    AddReserve(
+        {
+            branch           => $branchcode,
+            borrowernumber   => $borrowernumber,
+            biblionumber     => $biblionumber,
+            priority         => $priority,
+            reservation_date => $reservation_date,
+            expiration_date  => $expiration_date,
+            notes            => $notes,
+            title            => $title,
+            itemnumber       => $itemnumber,
+            found            => $found,
+            itemtype         => $itemtype,
+        }
+    );
 
 Adds reserve and generates HOLDPLACED message.
 
@@ -159,11 +171,18 @@ The following tables are available witin the HOLDPLACED message:
 =cut
 
 sub AddReserve {
-    my (
-        $branch,   $borrowernumber, $biblionumber, $bibitems,
-        $priority, $resdate,        $expdate,      $notes,
-        $title,    $checkitem,      $found,        $itemtype
-    ) = @_;
+    my ($params)       = @_;
+    my $branch         = $params->{branchcode};
+    my $borrowernumber = $params->{borrowernumber};
+    my $biblionumber   = $params->{biblionumber};
+    my $priority       = $params->{priority};
+    my $resdate        = $params->{reservation_date};
+    my $expdate        = $params->{expiration_date};
+    my $notes          = $params->{notes};
+    my $title          = $params->{title};
+    my $checkitem      = $params->{itemnumber};
+    my $found          = $params->{found};
+    my $itemtype       = $params->{itemtype};
 
     $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
         or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
@@ -173,9 +192,25 @@ sub AddReserve {
     # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
     # of the document, we force the value $priority and $found .
     if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
-        $priority = 0;
         my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
-        if ( $item->holdingbranch eq $branch ) {
+
+        if (
+            # If item is already checked out, it cannot be set waiting
+            !$item->onloan
+
+            # The item can't be waiting if it needs a transfer
+            && $item->holdingbranch eq $branch
+
+            # Similarly, if in transit it can't be waiting
+            && !$item->get_transfer
+
+            # If we can't hold damaged items, and it is damaged, it can't be waiting
+            && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
+
+            # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
+            && !$item->current_holds->count )
+        {
+            $priority = 0;
             $found = 'W';
         }
     }
@@ -189,7 +224,7 @@ sub AddReserve {
     my $waitingdate;
 
     # If the reserv had the waiting status, we had the value of the resdate
-    if ( $found eq 'W' ) {
+    if ( $found && $found eq 'W' ) {
         $waitingdate = $resdate;
     }
 
@@ -213,7 +248,7 @@ sub AddReserve {
             item_level_hold => $checkitem ? 1 : 0,
         }
     )->store();
-    $hold->set_waiting() if $found eq 'W';
+    $hold->set_waiting() if $found && $found eq 'W';
 
     logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
         if C4::Context->preference('HoldsLog');
@@ -299,11 +334,13 @@ sub CanBookBeReserved{
          { status => ageRestricted },   if the Item is age restricted for this borrower.
          { status => damaged },         if the Item is damaged.
          { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
+         { status => branchNotInHoldGroup }, if borrower home library is not in hold group, and holds are only allowed from hold groups.
          { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
          { status => notReservable },   if holds on this item are not allowed
          { status => libraryNotFound },   if given branchcode is not an existing library
          { status => libraryNotPickupLocation },   if given branchcode is not configured to be a pickup location
          { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
+         { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
 
 =cut
 
@@ -363,22 +400,22 @@ sub CanItemBeReserved {
     # we retrieve rights
     if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->effective_itemtype, $branchcode ) ) {
         $ruleitemtype     = $rights->{itemtype};
-        $allowedreserves  = $rights->{reservesallowed};
-        $holds_per_record = $rights->{holds_per_record};
+        $allowedreserves  = $rights->{reservesallowed} // $allowedreserves;
+        $holds_per_record = $rights->{holds_per_record} // $holds_per_record;
         $holds_per_day    = $rights->{holds_per_day};
     }
     else {
-        $ruleitemtype = '*';
+        $ruleitemtype = undef;
     }
 
     my $holds = Koha::Holds->search(
         {
             borrowernumber => $borrowernumber,
             biblionumber   => $item->biblionumber,
-            found          => undef, # Found holds don't count against a patron's holds limit
         }
     );
-    if ( $holds->count() >= $holds_per_record ) {
+    if (   defined $holds_per_record && $holds_per_record ne ''
+        && $holds->count() >= $holds_per_record ) {
         return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
     }
 
@@ -387,10 +424,9 @@ sub CanItemBeReserved {
         reservedate    => dt_from_string->date
     });
 
-    if ( defined $holds_per_day &&
-          (   ( $holds_per_day > 0 && $today_holds->count() >= $holds_per_day )
-           or ( $holds_per_day == 0 ) )
-        )  {
+    if (   defined $holds_per_day && $holds_per_day ne ''
+        && $today_holds->count() >= $holds_per_day )
+    {
         return { status => 'tooManyReservesToday', limit => $holds_per_day };
     }
 
@@ -404,15 +440,15 @@ sub CanItemBeReserved {
       C4::Context->preference('item-level_itypes')
       ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
       : " AND biblioitems.itemtype = ?"
-      if ( $ruleitemtype ne "*" );
+      if defined $ruleitemtype;
 
     my $sthcount = $dbh->prepare($querycount);
 
-    if ( $ruleitemtype eq "*" ) {
-        $sthcount->execute( $borrowernumber, $branchcode );
+    if ( defined $ruleitemtype ) {
+        $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
     }
     else {
-        $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
+        $sthcount->execute( $borrowernumber, $branchcode );
     }
 
     my $reservecount = "0";
@@ -421,7 +457,8 @@ sub CanItemBeReserved {
     }
 
     # we check if it's ok or not
-    if ( $reservecount >= $allowedreserves ) {
+    if (   defined  $allowedreserves && $allowedreserves ne ''
+        && $reservecount >= $allowedreserves ) {
         return { status => 'tooManyReserves', limit => $allowedreserves };
     }
 
@@ -458,6 +495,13 @@ sub CanItemBeReserved {
         return { status => 'cannotReserveFromOtherBranches' };
     }
 
+    my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
+    if ( $branchitemrule->{holdallowed} == 3) {
+        if($borrower->{branchcode} ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $borrower->{branchcode}} )) {
+            return { status => 'branchNotInHoldGroup' };
+        }
+    }
+
     # If reservecount is ok, we check item branch if IndependentBranches is ON
     # and canreservefromotherbranches is OFF
     if ( C4::Context->preference('IndependentBranches')
@@ -482,6 +526,12 @@ sub CanItemBeReserved {
         unless ($item->can_be_transferred({ to => $destination })) {
             return { status => 'cannotBeTransferred' };
         }
+        unless ($branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
+            return { status => 'pickupNotInHoldGroup' };
+        }
+        unless ($branchitemrule->{hold_fulfillment_policy} ne 'patrongroup' || Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
+            return { status => 'pickupNotInHoldGroup' };
+        }
     }
 
     return { status => 'OK' };
@@ -578,7 +628,7 @@ sub ChargeReserveFee {
             library_id   => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
             interface    => C4::Context->interface,
             invoice_type => undef,
-            type         => 'reserve',
+            type         => 'RESERVE',
             item_id      => undef
         }
     );
@@ -656,7 +706,7 @@ sub GetReserveStatus {
         return 'Finished' if $found eq 'F';
     }
 
-    return 'Reserved' if $priority > 0;
+    return 'Reserved' if defined $priority && $priority > 0;
 
     return ''; # empty string here will remove need for checking undef, or less log lines
 }
@@ -758,7 +808,7 @@ sub CheckReserves {
 
         my $priority = 10000000;
         foreach my $res (@reserves) {
-            if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
+            if ( $res->{'itemnumber'} && $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
                 if ($res->{'found'} eq 'W') {
                     return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
                 } else {
@@ -795,9 +845,13 @@ sub CheckReserves {
                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
                     next if ($branchitemrule->{'holdallowed'} == 0);
                     next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
+                    my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
+                    next if (($branchitemrule->{'holdallowed'} == 3) && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
                     my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
-                    next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
-                    next unless $item->can_be_transferred( { to => scalar Koha::Libraries->find( $res->{branchcode} ) } );
+                    next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
+                    next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
+                    next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
+                    next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
                     $priority = $res->{'priority'};
                     $highest  = $res;
                     last if $local_hold_match;
@@ -1033,7 +1087,9 @@ sub ModReserveStatus {
     $sth_set->execute( $newstatus, $itemnumber );
 
     my $item = Koha::Items->find($itemnumber);
-    if ( ( $item->location eq 'CART' && $item->permanent_location ne 'CART'  ) && $newstatus ) {
+    if ( $item->location && $item->location eq 'CART'
+        && ( !$item->permanent_location || $item->permanent_location ne 'CART' )
+        && $newstatus ) {
       CartToShelf( $itemnumber );
     }
 }
@@ -1086,7 +1142,8 @@ sub ModReserveAffect {
 
     _FixPriority( { biblionumber => $biblionumber } );
     my $item = Koha::Items->find($itemnumber);
-    if ( ( $item->location eq 'CART' && $item->permanent_location ne 'CART'  ) ) {
+    if ( $item->location && $item->location eq 'CART'
+        && ( !$item->permanent_location || $item->permanent_location ne 'CART' ) ) {
       CartToShelf( $itemnumber );
     }
 
@@ -1183,13 +1240,19 @@ sub IsAvailableForItemLevelRequest {
         $item->withdrawn        ||
         ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
 
-    my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
+    my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
 
     if ($pickup_branchcode) {
         my $destination = Koha::Libraries->find($pickup_branchcode);
         return 0 unless $destination;
         return 0 unless $destination->pickup_location;
         return 0 unless $item->can_be_transferred( { to => $destination } );
+        my $reserves_control_branch =
+            GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
+        my $branchitemrule =
+            C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
+        my $home_library = Koka::Libraries->find( {branchcode => $item->homebranch} );
+        return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
     }
 
     if ( $on_shelf_holds == 1 ) {
@@ -1203,6 +1266,8 @@ sub IsAvailableForItemLevelRequest {
         foreach my $i (@items) {
             my $reserves_control_branch = GetReservesControlBranch( $i->unblessed(), $patron->unblessed );
             my $branchitemrule = C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
+            my $item_library = Koha::Libraries->find( {branchcode => $i->homebranch} );
+
 
             $any_available = 1
               unless $i->itemlost
@@ -1213,7 +1278,8 @@ sub IsAvailableForItemLevelRequest {
               || ( $i->damaged
                 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
               || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
-              || $branchitemrule->{holdallowed} == 1 && $patron->branchcode ne $i->homebranch;
+              || $branchitemrule->{holdallowed} == 1 && $patron->branchcode ne $i->homebranch
+              || $branchitemrule->{holdallowed} == 3 && !$item_library->validate_hold_sibling( {branchcode => $patron->branchcode} );
         }
 
         return $any_available ? 0 : 1;
@@ -1429,7 +1495,7 @@ sub _FixPriority {
     if ( $rank eq "del" ) { # FIXME will crash if called without $hold
         $hold->cancel;
     }
-    elsif ( $rank eq "W" || $rank eq "0" ) {
+    elsif ( $reserve_id && ( $rank eq "W" || $rank eq "0" ) ) {
 
         # make sure priority for waiting or in-transit items is 0
         my $query = "
@@ -1457,11 +1523,12 @@ sub _FixPriority {
         push( @priority,     $line );
     }
 
+    # FIXME This whole sub must be rewritten, especially to highlight what is done when reserve_id is not given
     # To find the matching index
     my $i;
     my $key = -1;    # to allow for 0 to be a valid result
     for ( $i = 0 ; $i < @priority ; $i++ ) {
-        if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
+        if ( $reserve_id && $reserve_id == $priority[$i]->{'reserve_id'} ) {
             $key = $i;    # save the index
             last;
         }
@@ -1790,6 +1857,8 @@ If $cancelreserve boolean is set to true, it will remove existing reserve
 sub MoveReserve {
     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
 
+    $cancelreserve //= 0;
+
     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
     return unless $res;
@@ -1918,24 +1987,18 @@ sub RevertWaitingStatus {
     $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'} );
-
-    unless ( $hold->item_level_hold ) {
-        $hold->itemnumber(undef)->store;
-    }
+    $hold->set(
+        {
+            priority    => 1,
+            found       => undef,
+            waitingdate => undef,
+            itemnumber  => $hold->item_level_hold ? $hold->itemnumber : undef,
+        }
+    )->store();
 
     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
+
+    return $hold;
 }
 
 =head2 ReserveSlip
@@ -2160,24 +2223,41 @@ patron category, itemtype, and library.
 sub GetHoldRule {
     my ( $categorycode, $itemtype, $branchcode ) = @_;
 
-    my $dbh = C4::Context->dbh;
-
-    my $sth = $dbh->prepare(
-        q{
-         SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record, holds_per_day
-           FROM issuingrules
-          WHERE (categorycode in (?,'*') )
-            AND (itemtype IN (?,'*'))
-            AND (branchcode IN (?,'*'))
-       ORDER BY categorycode DESC,
-                itemtype     DESC,
-                branchcode   DESC
+    my $reservesallowed = Koha::CirculationRules->get_effective_rule(
+        {
+            itemtype     => $itemtype,
+            categorycode => $categorycode,
+            branchcode   => $branchcode,
+            rule_name    => 'reservesallowed',
+            order_by     => {
+                -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
+            }
         }
     );
 
-    $sth->execute( $categorycode, $itemtype, $branchcode );
+    my $rules;
+    if ( $reservesallowed ) {
+        $rules->{reservesallowed} = $reservesallowed->rule_value;
+        $rules->{itemtype}        = $reservesallowed->itemtype;
+        $rules->{categorycode}    = $reservesallowed->categorycode;
+        $rules->{branchcode}      = $reservesallowed->branchcode;
+    }
+
+    my $holds_per_x_rules = Koha::CirculationRules->get_effective_rules(
+        {
+            itemtype     => $itemtype,
+            categorycode => $categorycode,
+            branchcode   => $branchcode,
+            rules        => ['holds_per_record', 'holds_per_day'],
+            order_by     => {
+                -desc => [ 'categorycode', 'itemtype', 'branchcode' ]
+            }
+        }
+    );
+    $rules->{holds_per_record} = $holds_per_x_rules->{holds_per_record};
+    $rules->{holds_per_day} = $holds_per_x_rules->{holds_per_day};
 
-    return $sth->fetchrow_hashref();
+    return $rules;
 }
 
 =head1 AUTHOR