Check for filled hold on transit checkin
[transitory.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / Circulate.pm
index 5da8ada..83a46a8 100644 (file)
@@ -14,6 +14,7 @@ my %scripts;
 my $script_libs;
 my $legacy_script_support = 0;
 my $booking_status;
+my $opac_renewal_use_circ_lib;
 
 sub determine_booking_status {
     unless (defined $booking_status) {
@@ -192,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 = 
@@ -534,6 +536,13 @@ my @AUTOLOAD_FIELDS = qw/
     skip_rental_fee
     use_booking
     generate_lost_overdue
+    clear_expired
+    retarget_mode
+    hold_as_transit
+    fake_hold_dest
+    limit_groups
+    override_args
+    checkout_is_for_hold
 /;
 
 
@@ -970,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;
 
@@ -981,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 , 
@@ -991,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");
@@ -1089,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 {
@@ -1112,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);
@@ -1183,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);
@@ -1359,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) 
@@ -1367,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);
+        }
    }
 }
     
@@ -1400,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');   
@@ -1482,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;
@@ -1614,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
@@ -1648,6 +1737,9 @@ sub handle_checkout_holds {
         $hold->clear_prev_check_time; 
         $hold->clear_current_copy;
         $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);
@@ -1660,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.
@@ -1680,8 +1774,17 @@ sub handle_checkout_holds {
 # ------------------------------------------------------------------------------
 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
 # the patron directly targets the checked out item, see if there is another hold 
-# (with hold_type T or V) for the patron that could be fulfilled by the checked 
-# out item.  Fulfill the oldest hold and only fulfill 1 of them.
+# for the patron that could be fulfilled by the checked out item.  Fulfill the
+# oldest hold and only fulfill 1 of them.
+# 
+# For "another hold":
+#
+# First, check for one that the copy matches via hold_copy_map, ensuring that
+# *any* hold type that this copy could fill may end up filled.
+#
+# Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
+# for a Title (T) or Volume (V) hold that matches the item. This allows items
+# that are non-requestable to count as capturing those hold types.
 # ------------------------------------------------------------------------------
 sub find_related_user_hold {
     my($self, $copy, $patron) = @_;
@@ -1697,6 +1800,51 @@ sub find_related_user_hold {
         select => {ahr => ['id']}, 
         from => {
             ahr => {
+                ahcm => {
+                    field => 'hold',
+                    fkey => 'id'
+                },
+                acp => {
+                    field => 'id', 
+                    fkey => 'current_copy',
+                    type => 'left' # there may be no current_copy
+                }
+            }
+        }, 
+        where => {
+            '+ahr' => {
+                usr => $patron->id,
+                fulfillment_time => undef,
+                cancel_time => undef,
+               '-or' => [
+                    {expire_time => undef},
+                    {expire_time => {'>' => 'now'}}
+                ]
+            },
+            '+ahcm' => {
+                target_copy => $self->copy->id
+            },
+            '+acp' => {
+                '-or' => [
+                    {id => undef}, # left-join copy may be nonexistent
+                    {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
+                ]
+            }
+        },
+        order_by => {ahr => {request_time => {direction => 'asc'}}},
+        limit => 1
+    };
+
+    my $hold_info = $e->json_query($args)->[0];
+    return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
+    return undef if $U->ou_ancestor_setting_value(        
+        $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
+
+    # find the oldest unfulfilled hold that has not yet hit the holds shelf.
+    $args = {
+        select => {ahr => ['id']}, 
+        from => {
+            ahr => {
                 acp => {
                     field => 'id', 
                     fkey => 'current_copy',
@@ -1739,7 +1887,7 @@ sub find_related_user_hold {
         limit => 1
     };
 
-    my $hold_info = $e->json_query($args)->[0];
+    $hold_info = $e->json_query($args)->[0];
     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
     return undef;
 }
@@ -2007,7 +2155,7 @@ sub booking_adjusted_due_date {
         my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
         my $bookings = $booking_ses->request(
             'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
-            { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
+            { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
         )->gather(1);
         $booking_ses->disconnect;
         
@@ -2240,6 +2388,9 @@ sub check_transit_checkin_interval {
         )->[0]
     ); 
 
+    # transit from X to X for whatever reason has no min interval
+    return if $self->transit->source == $self->transit->dest;
+
     my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
     my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
     my $horizon = $t_start->add(seconds => $seconds);
@@ -2249,6 +2400,92 @@ sub check_transit_checkin_interval {
         if $horizon > DateTime->now;
 }
 
+# Retarget local holds at checkin
+sub checkin_retarget {
+    my $self = shift;
+    return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
+    return unless $self->is_checkin; # Renewals need not be checked
+    return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
+    return if $self->is_precat; # No holds for precats
+    return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
+    return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
+    my $status = $U->copy_status($self->copy->status);
+    return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
+    # Specifically target items that are likely new (by status ID)
+    return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
+    my $location = $self->copy->location;
+    if(!ref($location)) {
+        $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
+        $self->copy->location($location);
+    }
+    return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
+
+    # Fetch holds for the bib
+    my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
+                    $self->editor->authtoken,
+                    $self->title->id,
+                    {
+                        capture_time => undef, # No touching captured holds
+                        frozen => 'f', # Don't bother with frozen holds
+                        pickup_lib => $self->circ_lib # Only holds actually here
+                    }); 
+
+    # Error? Skip the step.
+    return if exists $result->{"ilsevent"};
+
+    # Assemble holds
+    my $holds = [];
+    foreach my $holdlist (keys %{$result}) {
+        push @$holds, @{$result->{$holdlist}};
+    }
+
+    return if scalar(@$holds) == 0; # No holds, no retargeting
+
+    # 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 {$_->part, 1} @$parts if @$parts;
+
+    # Loop over holds in request-ish order
+    # Stage 1: Get them into request-ish order
+    # Also grab type and target for skipping low hanging ones
+    $result = $self->editor->json_query({
+        "select" => { "ahr" => ["id", "hold_type", "target"] },
+        "from" => { "ahr" => { "au" => { "fkey" => "usr",  "join" => "pgt"} } },
+        "where" => { "id" => $holds },
+        "order_by" => [
+            { "class" => "pgt", "field" => "hold_priority"},
+            { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
+            { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
+            { "class" => "ahr", "field" => "request_time"}
+        ]
+    });
+
+    # Stage 2: Loop!
+    if (ref $result eq "ARRAY" and scalar @$result) {
+        foreach (@{$result}) {
+            # Copy level, but not this copy?
+            next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
+                and $_->{target} != $self->copy->id);
+            # Volume level, but not this volume?
+            next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
+            if(@$parts) { # We have parts?
+                # Skip title holds
+                next if ($_->{hold_type} eq 'T');
+                # Skip part holds for parts not on this copy
+                next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
+            } else {
+                # No parts, no part holds
+                next if ($_->{hold_type} eq 'P');
+            }
+            # So much for easy stuff, attempt a retarget!
+            my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
+            if(ref $tresult eq "ARRAY" and scalar @$tresult) {
+                last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
+            }
+        }
+    }
+}
 
 sub do_checkin {
     my $self = shift;
@@ -2259,6 +2496,7 @@ sub do_checkin {
         unless $self->copy;
 
     $self->check_transit_checkin_interval;
+    $self->checkin_retarget;
 
     # the renew code and mk_env should have already found our circulation object
     unless( $self->circ ) {
@@ -2279,6 +2517,9 @@ sub do_checkin {
     if( $self->checkin_check_holds_shelf() ) {
         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
+        if($self->fake_hold_dest) {
+            $self->hold->pickup_lib($self->circ_lib);
+        }
         $self->checkin_flesh_events;
         return;
     }
@@ -2347,17 +2588,32 @@ 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
+                if($self->fake_hold_dest) {
+                    $hold->pickup_lib($self->circ_lib);
+                }
                 $self->checkin_flesh_events;
                 return;
             }
@@ -2424,8 +2680,21 @@ sub do_checkin {
             }
     
             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
-    
-            if( $circ_lib == $self->circ_lib) {
+
+            my $suppress_transit = 0;
+
+            if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
+                my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
+                if($suppress_transit_source && $suppress_transit_source->{value}) {
+                    my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
+                    if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
+                        $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
+                        $suppress_transit = 1;
+                    }
+                }
+            }
+            if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
                 # copy is where it needs to be, either for hold or reshelving
     
                 $self->checkin_handle_precat();
@@ -2562,6 +2831,10 @@ sub checkin_check_holds_shelf {
         $U->copy_status($self->copy->status)->id ==
             OILS_COPY_STATUS_ON_HOLDS_SHELF;
 
+    # Attempt to clear shelf expired holds for this copy
+    $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
+        if($self->clear_expired);
+
     # find the hold that put us on the holds shelf
     my $holds = $self->editor->search_action_hold_request(
         { 
@@ -2583,7 +2856,19 @@ sub checkin_check_holds_shelf {
     $logger->info("circulator: we found a captured, un-fulfilled hold [".
         $hold->id. "] for copy ".$self->copy->barcode);
 
-    if( $hold->pickup_lib == $self->circ_lib ) {
+    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}) {
+                $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
+                $self->fake_hold_dest(1);
+                return 1;
+            }
+        }
+    }
+
+    if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
         return 1;
     }
@@ -2611,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);
 }
@@ -2690,7 +2994,11 @@ sub attempt_checkin_hold_capture {
 
     if($self->capture ne 'capture') {
         # see if this item is in a hold-capture-delay location
-        my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
+        my $location = $self->copy->location;
+        if(!ref($location)) {
+            $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
+            $self->copy->location($location);
+        }
         if($U->is_true($location->hold_verify)) {
             $self->bail_on_events(
                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
@@ -2700,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
@@ -2723,14 +3043,22 @@ sub attempt_checkin_hold_capture {
 
     return 0 if $self->bail_out;
 
-    if( $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 {
     
@@ -2744,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;
 }
 
@@ -2891,8 +3220,22 @@ sub process_received_transit {
 
     my $transit = $self->transit;
 
-    if( $transit->dest != $self->circ_lib ) {
+    # Check if we are in a transit suppress range
+    my $suppress_transit = 0;
+    if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
+        my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ?  'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
+        my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
+        if($suppress_transit_circ && $suppress_transit_circ->{value}) {
+            my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
+            if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
+                $suppress_transit = 1;
+                $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
+            }
+        }
+    }
+    if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
         # - this item is in-transit to a different location
+        # - Or we are capturing holds as transits, so why create a new transit?
 
         my $tid = $transit->id; 
         my $loc = $self->circ_lib;
@@ -2952,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;
 }
 
@@ -3079,26 +3396,18 @@ sub checkin_handle_circ {
         $self->copy->circ_lib->id : $self->copy->circ_lib;
     my $stat = $U->copy_status($self->copy->status)->id;
 
-    # immediately available keeps items lost or missing items from going home before being handled
-    my $lost_immediately_available = $U->ou_ancestor_setting_value(
-        $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
-
-
-    if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
-
-        if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
-            $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
-        } else {
-            $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
-            $self->update_copy;
-        }
-
-    } elsif ($stat == OILS_COPY_STATUS_LOST) {
-
+    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;
     }
@@ -3157,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;
+    }
 }
 
 
@@ -3318,10 +3644,16 @@ sub do_renew {
 
     # Opac renewal - re-use circ library from original circ (unless told not to)
     if($self->opac_renewal) {
-        my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
-        if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
-            $self->circ_lib($circ->circ_lib);
+        unless(defined($opac_renewal_use_circ_lib)) {
+            my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
+            if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
+                $opac_renewal_use_circ_lib = 1;
+            }
+            else {
+                $opac_renewal_use_circ_lib = 0;
+            }
         }
+        $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
     }
 
     # Run the fine generator against the old circ