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) {
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 =
$circulator->editor->rollback;
} else {
+
$circulator->editor->commit;
- }
- $circulator->script_runner->cleanup if $circulator->script_runner;
+ if ($circulator->generate_lost_overdue) {
+ # Generating additional overdue billings has to happen after the
+ # main commit and before the final respond() so the caller can
+ # receive the latest transaction summary.
+ my $evt = $circulator->generate_lost_overdue_fines;
+ $circulator->bail_on_events($evt) if $evt;
+ }
+ }
$conn->respond_complete(circ_events($circulator));
- unless($circulator->bail_out) {
- $circulator->do_hold_notify($circulator->notify_hold)
- if $circulator->notify_hold;
- $circulator->retarget_holds if $circulator->retarget;
- $circulator->append_reading_list;
- $circulator->make_trigger_events;
- }
+ $circulator->script_runner->cleanup if $circulator->script_runner;
+
+ return undef if $circulator->bail_out;
+
+ $circulator->do_hold_notify($circulator->notify_hold)
+ if $circulator->notify_hold;
+ $circulator->retarget_holds if $circulator->retarget;
+ $circulator->append_reading_list;
+ $circulator->make_trigger_events;
+
+ return undef;
}
sub circ_events {
skip_deposit_fee
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
/;
my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
- $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
+ $patron = $e->retrieve_actor_user($card->usr)
or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
+ # Use the card we looked up, not the patron's primary, for card active checks
+ $patron->card($card);
+
} else {
if( my $copy = $self->copy ) {
}
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;
$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 ,
},
{ 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");
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 {
$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);
$logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
$self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
$self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
- if($results->[0]->{renewals}) {
+ if(defined($results->[0]->{renewals})) {
$self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
}
$self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
- if($results->[0]->{grace_period}) {
+ if(defined($results->[0]->{grace_period})) {
$self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
}
$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);
my $self = shift;
my @events = @{$self->events};
return unless @events;
+ my $oargs = $self->override_args;
if(!$self->override) {
return $self->bail_out(1)
$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);
+ }
}
}
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');
# 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;
$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
$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);
$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.
# ------------------------------------------------------------------------------
# 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) = @_;
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',
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;
}
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;
}
}
+# If a copy goes into transit and is then checked in before the transit checkin
+# interval has expired, push an event onto the overridable events list.
+sub check_transit_checkin_interval {
+ my $self = shift;
+
+ # only concerned with in-transit items
+ return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
+
+ # no interval, no problem
+ my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
+ return unless $interval;
+
+ # capture the transit so we don't have to fetch it again later during checkin
+ $self->transit(
+ $self->editor->search_action_transit_copy(
+ {target_copy => $self->copy->id, dest_recv_time => undef}
+ )->[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);
+
+ # See if we are still within the transit checkin forbidden range
+ $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
+ 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;
OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
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 ) {
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;
}
$self->override_events unless $self->is_renewal;
return if $self->bail_out;
- if( $self->copy ) {
+ if( $self->copy and !$self->transit ) {
$self->transit(
$self->editor->search_action_transit_copy(
{ target_copy => $self->copy->id, dest_recv_time => undef }
}
if( $self->circ ) {
+ $self->generate_fines_finish;
$self->checkin_handle_circ;
return if $self->bail_out;
$self->checkin_changed(1);
$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;
}
}
$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();
$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(
{
$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;
}
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);
}
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));
$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
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 {
# make sure we save the copy status
$self->update_copy;
+ return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
return 1;
}
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;
# ------------------------------------------------------------------
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);
-
- if($shelf_expire) {
- my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
- my $expire_time = DateTime->now->add(seconds => $seconds);
- $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;
}
$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;
}
$circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
my $restore_od = $U->ou_ancestor_setting_value(
$circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
+ $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
+ $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
$self->checkin_handle_lost_now_found(3) if $void_lost;
$self->checkin_handle_lost_now_found(4) if $void_lost_fee;
- $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
+ $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;
+ }
}
my $circ = $self->editor->search_action_circulation({
target_copy => $self->copy->id,
xact_finish => undef,
+ checkin_time => undef,
($usrid ? (usr => $usrid) : ()),
'-or' => [
{stop_fines => undef},
$self->renewal_remaining( $circ->renewal_remaining - 1 );
$self->circ($circ);
+ # Opac renewal - re-use circ library from original circ (unless told not to)
+ if($self->opac_renewal) {
+ 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
$self->generate_fines_start;
sub checkin_handle_lost_now_found_restore_od {
my $self = shift;
+ my $circ_lib = shift;
# ------------------------------------------------------------------
# restore those overdue charges voided when item was set to lost
}
}
+# ------------------------------------------------------------------
+# Lost-then-found item checked in. This sub generates new overdue
+# fines, beyond the point of any existing and possibly voided
+# overdue fines, up to the point of final checkin time (or max fine
+# amount).
+# ------------------------------------------------------------------
+sub generate_lost_overdue_fines {
+ my $self = shift;
+ my $circ = $self->circ;
+ my $e = $self->editor;
+
+ # Re-open the transaction so the fine generator can see it
+ if($circ->xact_finish or $circ->stop_fines) {
+ $e->xact_begin;
+ $circ->clear_xact_finish;
+ $circ->clear_stop_fines;
+ $circ->clear_stop_fines_time;
+ $e->update_action_circulation($circ) or return $e->die_event;
+ $e->xact_commit;
+ }
+
+ $e->xact_begin; # generate_fines expects an in-xact editor
+ $self->generate_fines;
+ $circ = $self->circ; # generate fines re-fetches the circ
+
+ my $update = 0;
+
+ # Re-close the transaction if no money is owed
+ my ($obt) = $U->fetch_mbts($circ->id, $e);
+ if ($obt and $obt->balance_owed == 0) {
+ $circ->xact_finish('now');
+ $update = 1;
+ }
+
+ # Set stop fines if the fine generator didn't have to
+ unless($circ->stop_fines) {
+ $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
+ $circ->stop_fines_time('now');
+ $update = 1;
+ }
+
+ # update the event data sent to the caller within the transaction
+ $self->checkin_flesh_events;
+
+ if ($update) {
+ $e->update_action_circulation($circ) or return $e->die_event;
+ $e->commit;
+ } else {
+ $e->rollback;
+ }
+
+ return undef;
+}
+
1;