$logger->info("circulator: un-targeting hold ".$hold->id.
" because copy ".$copy->id." is getting checked out");
-
+ try {
+ $U->simplereq('open-ils.circ',
+ 'open-ils.circ.hold_reset_reason_entry.create',
+ $e->authtoken,
+ $hold->id,
+ OILS_HOLD_CHECK_OUT,
+ "Checked out to patron #".$patron->id);
+ } catch Error with {
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
$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;
- $U->simplereq('open-ils.circ',
- 'open-ils.circ.hold_reset_reason_entry.create',$e->authtoken,$hold->id,OILS_HOLD_CHECK_OUT,"Checked out to patron #".$patron->id);
return $self->bail_on_event($e->event)
unless $e->update_action_hold_request($hold);
next if ($_->{hold_type} eq 'P');
}
# So much for easy stuff, attempt a retarget!
- $U->simplereq('open-ils.circ',
- 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $_->{id},OILS_HOLD_BETTER_HOLD);
+ try{
+ $U->simplereq('open-ils.circ',
+ 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $_->{id},OILS_HOLD_BETTER_HOLD);
+ }
+ catch Error with{
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
my $tresult = $U->simplereq(
'open-ils.hold-targeter',
'open-ils.hold-targeter.target',
$hold->clear_cancel_time;
$hold->clear_prev_check_time unless $hold->prev_check_time;
- $U->simplereq('open-ils.circ',
- 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $hold->id, OILS_HOLD_CHECK_IN);
+ try{
+ $U->simplereq('open-ils.circ',
+ 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $hold->id, OILS_HOLD_CHECK_IN);
+ }
+ catch Error with{
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
$self->bail_on_events($self->editor->event)
unless $self->editor->update_action_hold_request($hold);
$self->hold($hold);
my $self = shift;
$logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
- my $cses = OpenSRF::AppSession->create('open-ils.circ');
- $cses->request('open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $self->retarget,OILS_HOLD_BETTER_HOLD);
- $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
+ $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
+ try{
+ my $cses = OpenSRF::AppSession->create('open-ils.circ');
+ $cses->request('open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $self->retarget,OILS_HOLD_BETTER_HOLD);
+ }
+ catch Error with{
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
# no reason to wait for the return value
return;
}
$hold->cancel_time('now');
$hold->cancel_cause($cause);
$hold->cancel_note($note);
- my $note_body = "";
- if($cause){
- my $cancel_reason = "ID $cause";
- my $cancel_cause = $e->retrieve_action_hold_request_cancel_cause($cause);
- if($cancel_cause){
- $cancel_reason = $cancel_cause->label;
- }
- $note_body .= "Cancel Cause: $cancel_reason";
- }
- else{
- $note_body .= "Cancel reason unknown";
- }
- $note_body .= "," unless $note_body eq "" || $note eq "";
- $note_body .= " Cancel Note: \"$note\"" unless $note eq "";
+ my $note_body = "";
+ if($cause){
+ my $cancel_reason = "ID $cause";
+ my $cancel_cause = $e->retrieve_action_hold_request_cancel_cause($cause);
+ if($cancel_cause){
+ $cancel_reason = $cancel_cause->label;
+ }
+ $note_body .= "Cancel Cause: $cancel_reason";
+ }
+ else{
+ $note_body .= "Cancel reason unknown";
+ }
+ $note_body .= "," unless $note_body eq "" || $note eq "";
+ $note_body .= " Cancel Note: \"$note\"" unless $note eq "";
_create_reset_reason_entry($e,$hold,OILS_HOLD_CANCELED,$note_body);
$e->update_action_hold_request($hold)
or return $e->die_event;
if($U->is_true($hold->frozen)) {
$logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
- _create_reset_reason_entry($e,$hold,OILS_HOLD_FROZEN,$note_body) unless $U->is_true($orig_hold->frozen);
+ _create_reset_reason_entry($e,$hold,OILS_HOLD_FROZEN,$note_body) unless $U->is_true($orig_hold->frozen);
$hold->clear_current_copy;
$hold->clear_prev_check_time;
# Clear expire_time to prevent frozen holds from expiring.
{
my($self, $conn, $auth, $hold, $reset_reason, $note) = @_;
my $e = new_editor(authtoken => $auth, xact => 1);
- #checkauth to set the requestor (if available)
- $e->checkauth;
+ #checkauth to set the requestor (if available)
+ $e->checkauth;
my @holds;
if(ref $hold eq 'ARRAY'){
@holds = @{$hold};
@holds = ($hold);
}
for my $holdid (@holds){
- my ($hold, $evt) = $U->fetch_hold($holdid);
- return $evt if $evt;
- _create_reset_reason_entry($e, $hold, $reset_reason, $note);
+ try{
+ my ($hold, $evt) = $U->fetch_hold($holdid);
+ _create_reset_reason_entry($e, $hold, $reset_reason, $note) unless $evt;
+ }
+ catch Error with{
+ $logger->error("holds: create reset reason failed with ".shift());
+ };
}
$e->commit;
return 1;
sub _create_reset_reason_entry
{
- my($e, $hold, $reset_reason,$note) = @_;
+ my($e, $hold, $reset_reason,$note,$previous_copy) = @_;
my $ts = DateTime->now;
my $entry = Fieldmapper::action::hold_request_reset_reason_entry->new;
$logger->info("Creating reset reason entry for hold #" . $hold->id);
- my $last_copy = $hold->current_copy;
+ my $last_copy = defined $previous_copy ? $previous_copy : $hold->current_copy;
$entry->hold($hold->id);
$entry->reset_reason($reset_reason);
$entry->reset_time('now');
- $entry->requestor($e->requestor->id) if defined $e->requestor;
- $entry->requestor_workstation($e->requestor->wsid) if defined $e->requestor;
$entry->previous_copy($last_copy);
$entry->note($note);
+ $entry->requestor($e->requestor->id) if defined $e->requestor;
+ $entry->requestor_workstation($e->requestor->wsid) if defined $e->requestor;
$e->create_action_hold_request_reset_reason_entry($entry) or return $e->die_event;
return 1;
}
my $single =
OpenILS::Utils::HoldTargeter::Single->new(parent => $targeter);
- # If targeter is issued without a hold
- # it will retarget all of the holds that need it
- # so we shoot off a RRE for all them.
- $hold_ses->request(
- "open-ils.circ.hold_reset_reason_entry.create",
- $single->editor()->authtoken,
- $hold_id,
- OILS_HOLD_TIMED_OUT)
- unless defined $args->{hold};
-
# Don't let an explosion on a single hold stop processing
eval { $single->target($hold_id) };
$logger->error($msg);
$single->message($msg) unless $single->message;
}
+ else{
+ try{
+ # create a TIMED_OUT reset reason
+ # other types of resets are handled
+ # at their sources.
+ $hold_ses->request(
+ "open-ils.circ.hold_reset_reason_entry.create",
+ $single->editor()->authtoken,
+ $hold_id,
+ OILS_HOLD_TIMED_OUT,
+ $single->{previous_copy_id}
+ ) unless defined $args->{hold};
+ } catch Error with {
+ $logger->error(
+ "hold-targeter: create reset reason failed with ".shift()
+ );
+ }
+ }
if (($count % $throttle) == 0) {
# Time to reply to the caller. Return either the number
use OpenSRF::AppSession;
use OpenILS::Utils::DateTime qw/:datetime/;
use OpenSRF::Utils::Logger qw(:logger);
+use OpenILS::Const qw/:const/;
use OpenILS::Application::AppUtils;
use OpenILS::Utils::CStoreEditor qw/:funcs/;
};
}
+sub retarget_previous_targets_interval {
+ my ($self) = @_;
+ if (!defined($self->{retarget_previous_targets_interval}) || !$self->{retarget_previous_targets_interval}) {
+ # See if we have a global flag value for the interval
+ my $rri = $self->editor->search_config_global_flag({
+ name => 'circ.holds.retarget_previous_targets_interval',
+ enabled => 't'
+ })->[0];
+ # If no flag is present, default to 0 so feature is disabled
+ $self->{retarget_previous_targets_interval} = $rri ? $rri->value : 0;
+ }
+ return $self->{retarget_previous_targets_interval};
+}
+
# List of potential copies in the form of slim hashes. This list
# evolves as copies are filtered as they are deemed non-targetable.
sub copies {
sub compile_weighted_proximity_map {
my $self = shift;
+ my %copy_reset_map = {};
+ my %copy_timeout_map = {};
+ eval{
+ my $pt_interval = $self->retarget_previous_targets_interval();
+ if($pt_interval){
+ my $reset_cutoff_time = DateTime->now(time_zone => 'local')
+ ->subtract(days => $pt_interval);
+
+ # Collect reset reason info and previous copies.
+ # for this hold within the last time interval
+ my $reset_entries = $self->editor->json_query({
+ select => {ahrrre => ['reset_reason','reset_time','previous_copy']},
+ from => 'ahrrre',
+ where => {
+ hold => $self->hold_id,
+ previous_copy => {'!=' => undef},
+ reset_time => {'>=' => $reset_cutoff_time->strftime('%F %T%z')},
+ reset_reason => [OILS_HOLD_TIMED_OUT, OILS_HOLD_MANUAL_RESET]
+ }
+ });
+
+ # count how many times each copy
+ # was reset or timed out
+ for(@$reset_entries){
+ my $pc = $_->{previous_copy};
+ my $rr = $_->{reset_reason};
+ if($rr == OILS_HOLD_MANUAL_RESET){
+ $copy_reset_map{$pc} += 1;
+ }
+ elsif($rr == OILS_HOLD_TIMED_OUT){
+ $copy_timeout_map{$pc} += 1;
+ }
+ }
+ }
+ };
+
# Collect copy proximity info (generated via DB trigger)
# from our newly create copy maps.
my $hold_copy_maps = $self->editor->json_query({
my %copy_prox_map =
map {$_->{target_copy} => $_->{proximity}} @$hold_copy_maps;
-
+
+ # calculate the maximum proximity to make adjustments
+ my $max_prox = 0;
+ foreach(@$hold_copy_maps){
+ $max_prox =
+ $_->{proximity} > $max_prox ? $_->{proximity} : $max_prox;
+ }
+
# Pre-fetch the org setting value for all circ libs so that
# later calls can reference the cached value.
$self->parent->precache_batch_ou_settings($self->get_copy_circ_libs,
my %prox_map;
for my $copy_hash (@{$self->copies}) {
- my $prox = $copy_prox_map{$copy_hash->{id}};
+ my $copy_id = $copy_hash->{id};
+ my $prox = $copy_prox_map{$copy_id};
+ my $reset_count = $copy_reset_map{$copy_id};
+ my $timeout_count = $copy_timeout_map{$copy_id};
+
+ # make adjustments to proximity based on reset reason.
+ # manual resets get +max_prox each time
+ # this moves them to the end of the hold copy map.
+ # timeout resets only add one level of proximity
+ # so that copies can be inspected again later.
+ $prox += (($reset_count || 0) * $max_prox) + (($timeout_count || 0));
+
$copy_hash->{proximity} = $prox;
$prox_map{$prox} ||= [];
--- /dev/null
+CREATE TABLE action.hold_request_reset_reason
+(
+ id serial NOT NULL,
+ manual boolean,
+ name text,
+CONSTRAINT hold_request_reset_reason_pkey PRIMARY KEY (id),
+CONSTRAINT hold_request_reset_reason_name_key UNIQUE (name)
+) WITH (
+ OIDS=FALSE
+);
+
+INSERT INTO action.hold_request_reset_reason (id, name, manual) VALUES
+(1,'HOLD_TIMED_OUT',false),
+(2,'HOLD_MANUAL_RESET',true),
+(3,'HOLD_BETTER_HOLD',false),
+(4,'HOLD_FROZEN',true),
+(5,'HOLD_UNFROZEN',true),
+(6,'HOLD_CANCELED',true),
+(7,'HOLD_UNCANCELED',true),
+(8,'HOLD_UPDATED',true),
+(9,'HOLD_CHECKED_OUT',true),
+(10,'HOLD_CHECKED_IN',true);
+
+CREATE TABLE action.hold_request_reset_reason_entry
+(
+ id serial NOT NULL,
+ hold int,
+ reset_reason int,
+ note text,
+ reset_time timestamp with time zone,
+ previous_copy bigint,
+ requestor int,
+ requestor_workstation int,
+ CONSTRAINT hold_request_reset_reason_entry_pkey PRIMARY KEY (id),
+ CONSTRAINT action_hold_request_reset_reason_entry_reason_fkey FOREIGN KEY (reset_reason)
+ REFERENCES action.hold_request_reset_reason (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+CONSTRAINT action_hold_request_reset_reason_entry_previous_copy_fkey FOREIGN KEY (previous_copy)
+ REFERENCES asset.copy (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+ CONSTRAINT action_hold_request_reset_reason_entry_requestor_fkey FOREIGN KEY (requestor)
+ REFERENCES actor.usr (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+ CONSTRAINT action_hold_request_reset_reason_entry_requestor_workstation_fkey FOREIGN KEY (requestor_workstation)
+ REFERENCES actor.workstation (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+ CONSTRAINT action_hold_request_reset_reason_entry_hold_fkey FOREIGN KEY (hold)
+ REFERENCES action.hold_request (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
+)
+
+WITH (
+ OIDS=FALSE
+);
+
+INSERT INTO config.global_flag (name, label, enabled)
+ VALUES (
+ 'circ.holds.retarget_previous_targets_interval',
+ oils_i18n_gettext(
+ 'circ.holds.retarget_previous_targets_interval',
+ 'Hold targeter will create proximity adjustments for previously targeted copies within this time interval (in days).',
+ 'cgf',
+ 'label'
+ ),
+ TRUE
+ );