Check for filled hold on transit checkin
[transitory.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / Circulate.pm
index e47d701..83a46a8 100644 (file)
@@ -193,6 +193,7 @@ __PACKAGE__->register_method(
 sub run_method {
     my( $self, $conn, $auth, $args ) = @_;
     translate_legacy_args($args);
+    $args->{override_args} = { all => 1 } unless defined $args->{override_args};
     my $api = $self->api_name;
 
     my $circulator = 
@@ -539,6 +540,9 @@ my @AUTOLOAD_FIELDS = qw/
     retarget_mode
     hold_as_transit
     fake_hold_dest
+    limit_groups
+    override_args
+    checkout_is_for_hold
 /;
 
 
@@ -975,9 +979,9 @@ sub is_group_descendant {
 }
 
 sub check_captured_holds {
-   my $self    = shift;
-   my $copy    = $self->copy;
-   my $patron  = $self->patron;
+    my $self    = shift;
+    my $copy    = $self->copy;
+    my $patron  = $self->patron;
 
     return undef unless $copy;
 
@@ -986,7 +990,7 @@ sub check_captured_holds {
     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
 
     # Item is on the holds shelf, make sure it's going to the right person
-    my $holds   = $self->editor->search_action_hold_request(
+    my $hold = $self->editor->search_action_hold_request(
         [
             { 
                 current_copy        => $copy->id , 
@@ -996,10 +1000,11 @@ sub check_captured_holds {
             },
             { limit => 1 }
         ]
-    );
+    )->[0];
 
-    if( $holds and $$holds[0] ) {
-        return undef if $$holds[0]->usr == $patron->id;
+    if ($hold and $hold->usr == $patron->id) {
+        $self->checkout_is_for_hold(1);
+        return undef;
     }
 
     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
@@ -1094,10 +1099,38 @@ sub run_patron_permit_scripts {
 
         my $results = $self->run_indb_circ_test;
         unless($self->circ_test_success) {
-            # no_item result is OK during noncat checkout
-            unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
-                push @allevents, $self->matrix_test_result_events;
+            my @trimmed_results;
+
+            if ($self->is_noncat) {
+                # no_item result is OK during noncat checkout
+                @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
+
+            } else {
+
+                if ($self->checkout_is_for_hold) {
+                    # if this checkout will fulfill a hold, ignore CIRC blocks
+                    # and rely instead on the (later-checked) FULFILL block
+
+                    my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
+                    my $fblock_pens = $self->editor->search_config_standing_penalty(
+                        {name => [@pen_names], block_list => {like => '%CIRC%'}});
+
+                    for my $res (@$results) {
+                        my $name = $res->{fail_part} || '';
+                        next if grep {$_->name eq $name} @$fblock_pens;
+                        push(@trimmed_results, $res);
+                    }
+
+                } else { 
+                    # not for hold or noncat
+                    @trimmed_results = @$results;
+                }
             }
+
+            # update the final set of test results
+            $self->matrix_test_result(\@trimmed_results); 
+
+            push @allevents, $self->matrix_test_result_events;
         }
 
     } else {
@@ -1117,6 +1150,8 @@ sub run_patron_permit_scripts {
         $penalties = $penalties->{fatal_penalties};
 
         for my $pen (@$penalties) {
+            # CIRC blocks are ignored if this is a FULFILL scenario
+            next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
             my $event = OpenILS::Event->new($pen->name);
             $event->{desc} = $pen->label;
             push(@allevents, $event);
@@ -1188,6 +1223,8 @@ sub run_indb_circ_test {
         }
         $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
         $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
+        # Grab the *last* response for limit_groups, where it is more likely to be filled
+        $self->limit_groups($results->[-1]->{limit_groups});
     }
 
     return $self->matrix_test_result($results);
@@ -1364,6 +1401,7 @@ sub override_events {
     my $self = shift;
     my @events = @{$self->events};
     return unless @events;
+    my $oargs = $self->override_args;
 
     if(!$self->override) {
         return $self->bail_out(1) 
@@ -1372,14 +1410,18 @@ sub override_events {
 
     $self->events([]);
     
-   for my $e (@events) {
-      my $tc = $e->{textcode};
-      next if $tc eq 'SUCCESS';
-      my $ov = "$tc.override";
-      $logger->info("circulator: attempting to override event: $ov");
+    for my $e (@events) {
+        my $tc = $e->{textcode};
+        next if $tc eq 'SUCCESS';
+        if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
+            my $ov = "$tc.override";
+            $logger->info("circulator: attempting to override event: $ov");
 
-        return $self->bail_on_events($self->editor->event)
-            unless( $self->editor->allowed($ov) );
+            return $self->bail_on_events($self->editor->event)
+                unless( $self->editor->allowed($ov) );
+        } else {
+            return $self->bail_out(1);
+        }
    }
 }
     
@@ -1405,7 +1447,7 @@ sub handle_claims_returned {
     my $evt;
 
     # - If the caller has set the override flag, we will check the item in
-    if($self->override) {
+    if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
 
         $CR->checkin_time('now');   
         $CR->checkin_scan_time('now');   
@@ -1487,6 +1529,10 @@ sub do_checkout {
     # refresh the circ to force local time zone for now
     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
 
+    if($self->limit_groups) {
+        $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
+    }
+
     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
     $self->update_copy;
     return if $self->bail_out;
@@ -1619,6 +1665,44 @@ sub bail_on_events {
     $self->bail_out(1);
 }
 
+# ------------------------------------------------------------------------------
+# A hold FULFILL block is just like a CIRC block, except that FULFILL only
+# affects copies that will fulfill holds and CIRC affects all other copies.
+# If blocks exists, bail, push Events onto the event pile, and return true.
+# ------------------------------------------------------------------------------
+sub check_hold_fulfill_blocks {
+    my $self = shift;
+
+    # See if the user has any penalties applied that prevent hold fulfillment
+    my $pens = $self->editor->json_query({
+        select => {csp => ['name', 'label']},
+        from => {ausp => {csp => {}}},
+        where => {
+            '+ausp' => {
+                usr => $self->patron->id,
+                org_unit => $U->get_org_full_path($self->circ_lib),
+                '-or' => [
+                    {stop_date => undef},
+                    {stop_date => {'>' => 'now'}}
+                ]
+            },
+            '+csp' => {block_list => {'like' => '%FULFILL%'}}
+        }
+    });
+
+    return 0 unless @$pens;
+
+    for my $pen (@$pens) {
+        $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
+        my $event = OpenILS::Event->new($pen->{name});
+        $event->{desc} = $pen->{label};
+        $self->push_events($event);
+    }
+
+    $self->override_events;
+    return $self->bail_out;
+}
+
 
 # ------------------------------------------------------------------------------
 # When an item is checked out, see if we can fulfill a hold for this patron
@@ -1655,6 +1739,7 @@ sub handle_checkout_holds {
         $hold->clear_capture_time;
         $hold->clear_shelf_time;
         $hold->clear_shelf_expire_time;
+           $hold->clear_current_shelf_lib;
 
         return $self->bail_on_event($e->event)
             unless $e->update_action_hold_request($hold);
@@ -1667,6 +1752,8 @@ sub handle_checkout_holds {
         $logger->info("circulator: found related hold to fulfill in checkout");
     }
 
+    return if $self->check_hold_fulfill_blocks;
+
     $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
 
     # if the hold was never officially captured, capture it.
@@ -2357,7 +2444,7 @@ sub checkin_retarget {
     # Check for parts on this copy
     my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
     my %parts_hash = ();
-    %parts_hash = map {$_->id, 1} @$parts if @$parts;
+    %parts_hash = map {$_->part, 1} @$parts if @$parts;
 
     # Loop over holds in request-ish order
     # Stage 1: Get them into request-ish order
@@ -2501,15 +2588,26 @@ sub do_checkin {
 
             $self->hold($hold);
 
-            if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
+            if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
 
-                $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
+                $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
                 $self->reshelve_copy(1);
                 $self->cancelled_hold_transit(1);
                 $self->notify_hold(0); # don't notify for cancelled holds
                 $self->fake_hold_dest(0);
                 return if $self->bail_out;
 
+            } elsif ($hold and $hold->hold_type eq 'R') {
+
+                $self->copy->status(OILS_COPY_STATUS_CATALOGING);
+                $self->notify_hold(0); # No need to notify
+                $self->fake_hold_dest(0);
+                $self->noop(1); # Don't try and capture for other holds/transits now
+                $self->update_copy();
+                $hold->fulfillment_time('now');
+                $self->bail_on_events($self->editor->event)
+                    unless $self->editor->update_action_hold_request($hold);
+
             } else {
 
                 # hold transited to correct location
@@ -2798,23 +2896,42 @@ sub checkin_build_copy_transit {
     my $self            = shift;
     my $dest            = shift;
     my $copy       = $self->copy;
-   my $transit    = Fieldmapper::action::transit_copy->new;
+    my $transit    = Fieldmapper::action::transit_copy->new;
+
+    # if we are transiting an item to the shelf shelf, it's a hold transit
+    if (my $hold = $self->remote_hold) {
+        $transit = Fieldmapper::action::hold_transit_copy->new;
+        $transit->hold($hold->id);
+
+        # the item is going into transit, remove any shelf-iness
+        if ($hold->current_shelf_lib or $hold->shelf_time) {
+            $hold->clear_current_shelf_lib;
+            $hold->clear_shelf_time;
+            return $self->bail_on_events($self->editor->event)
+                unless $self->editor->update_action_hold_request($hold);
+        }
+    }
 
     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
     $logger->info("circulator: transiting copy to $dest");
 
-   $transit->source($self->circ_lib);
-   $transit->dest($dest);
-   $transit->target_copy($copy->id);
-   $transit->source_send_time('now');
-   $transit->copy_status( $U->copy_status($copy->status)->id );
+    $transit->source($self->circ_lib);
+    $transit->dest($dest);
+    $transit->target_copy($copy->id);
+    $transit->source_send_time('now');
+    $transit->copy_status( $U->copy_status($copy->status)->id );
 
     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
 
-    return $self->bail_on_events($self->editor->event)
-        unless $self->editor->create_action_transit_copy($transit);
+    if ($self->remote_hold) {
+        return $self->bail_on_events($self->editor->event)
+            unless $self->editor->create_action_hold_transit_copy($transit);
+    } else {
+        return $self->bail_on_events($self->editor->event)
+            unless $self->editor->create_action_transit_copy($transit);
+    }
 
-   $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
+    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
     $self->update_copy;
     $self->checkin_changed(1);
 }
@@ -2891,12 +3008,24 @@ sub attempt_checkin_hold_capture {
 
     $self->retarget($retarget);
 
+    my $suppress_transit = 0;
+    if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
+        my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
+        if($suppress_transit_circ && $suppress_transit_circ->{value}) {
+            my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
+            if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
+                $suppress_transit = 1;
+                $self->hold->pickup_lib($self->circ_lib);
+            }
+        }
+    }
+
     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
 
     $hold->current_copy($copy->id);
     $hold->capture_time('now');
     $self->put_hold_on_shelf($hold) 
-        if $hold->pickup_lib == $self->circ_lib;
+        if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
 
     # prevent DB errors caused by fetching 
     # holds from storage, and updating through cstore
@@ -2914,26 +3043,22 @@ sub attempt_checkin_hold_capture {
 
     return 0 if $self->bail_out;
 
-    my $suppress_transit = 0;
-    if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
-        my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
-        if($suppress_transit_circ && $suppress_transit_circ->{value}) {
-            my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
-            if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
-                $suppress_transit = 1;
-                $self->hold->pickup_lib($self->circ_lib);
-            }
-        }
-    }
-
     if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
 
-        # This hold was captured in the correct location
-        $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
-        $self->push_events(OpenILS::Event->new('SUCCESS'));
+        if ($hold->hold_type eq 'R') {
+            $copy->status(OILS_COPY_STATUS_CATALOGING);
+            $hold->fulfillment_time('now');
+            $self->noop(1); # Block other transit/hold checks
+            $self->bail_on_events($self->editor->event)
+                unless $self->editor->update_action_hold_request($hold);
+        } else {
+            # This hold was captured in the correct location
+            $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
+            $self->push_events(OpenILS::Event->new('SUCCESS'));
 
-        #$self->do_hold_notify($hold->id);
-        $self->notify_hold($hold->id);
+            #$self->do_hold_notify($hold->id);
+            $self->notify_hold($hold->id);
+        }
 
     } else {
     
@@ -2947,6 +3072,7 @@ sub attempt_checkin_hold_capture {
 
     # make sure we save the copy status
     $self->update_copy;
+    return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
     return 1;
 }
 
@@ -3169,35 +3295,9 @@ sub process_received_transit {
 # ------------------------------------------------------------------
 sub put_hold_on_shelf {
     my($self, $hold) = @_;
-
     $hold->shelf_time('now');
-
-    my $shelf_expire = $U->ou_ancestor_setting_value(
-        $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
-
-    return undef unless $shelf_expire;
-
-    my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
-    my $expire_time = DateTime->now->add(seconds => $seconds);
-
-    # if the shelf expire time overlaps with a pickup lib's 
-    # closed date, push it out to the first open date
-    my $dateinfo = $U->storagereq(
-        'open-ils.storage.actor.org_unit.closed_date.overlap', 
-        $hold->pickup_lib, $expire_time);
-
-    if($dateinfo) {
-        my $dt_parser = DateTime::Format::ISO8601->new;
-        $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
-
-        # TODO: enable/disable time bump via setting?
-        $expire_time->set(hour => '23', minute => '59', second => '59');
-
-        $logger->info("circulator: shelf_expire_time overlaps".
-            " with closed date, pushing expire time to $expire_time");
-    }
-
-    $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
+    $hold->current_shelf_lib($self->circ_lib);
+    $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
     return undef;
 }
 
@@ -3297,11 +3397,17 @@ sub checkin_handle_circ {
     my $stat = $U->copy_status($self->copy->status)->id;
 
     if ($stat == OILS_COPY_STATUS_LOST) {
-
+        # we will now handle lost fines, but the copy will retain its 'lost'
+        # status if it needs to transit home unless lost_immediately_available
+        # is true
+        #
+        # if we decide to also delay fine handling until the item arrives home,
+        # we will need to call lost fine handling code both when checking items
+        # in and also when receiving transits
         $self->checkin_handle_lost($circ_lib);
-
+    } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
+        $logger->info("circulator: not updating copy status on checkin because copy is missing");
     } else {
-
         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
         $self->update_copy;
     }
@@ -3360,8 +3466,25 @@ sub checkin_handle_lost {
         $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
     }
 
-    $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
-    $self->update_copy;
+    if ($circ_lib != $self->circ_lib) {
+        # if the item is not home, check to see if we want to retain the lost
+        # status at this point in the process
+        my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
+
+        if ($immediately_available) {
+            # lost item status does not need to be retained, so give it a
+            # reshelving status as if it were a normal checkin
+            $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+            $self->update_copy;
+        } else {
+            $logger->info("circulator: not updating copy status on checkin because copy is lost");
+        }
+    } else {
+        # lost item is home and processed, treat like a normal checkin from
+        # this point on
+        $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+        $self->update_copy;
+    }
 }