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 =
retarget_mode
hold_as_transit
fake_hold_dest
+ limit_groups
+ override_args
+ checkout_is_for_hold
/;
}
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);
}
$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_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.
# 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
$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
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);
}
$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;
- 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 {
# make sure we save the copy status
$self->update_copy;
+ return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
return 1;
}
# ------------------------------------------------------------------
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;
}
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;
}
$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;
+ }
}