<field reporter:label="Requesting Library" name="request_lib" reporter:datatype="org_unit"/>
<field reporter:label="Request Date/Time" name="request_time" reporter:datatype="timestamp"/>
<field reporter:label="Requesting User" name="requestor" reporter:datatype="link"/>
+ <field reporter:label="Reset Entries" name="reset_entries" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Item Selection Depth" name="selection_depth" />
<field reporter:label="Selection Locus" name="selection_ou" reporter:datatype="org_unit"/>
<field reporter:label="Target Object ID" name="target" reporter:datatype="link"/>
<link field="request_lib" reltype="has_a" key="id" map="" class="aou"/>
<link field="transit" reltype="might_have" key="hold" map="" class="ahtc"/>
<link field="notifications" reltype="has_many" key="hold" map="" class="ahn"/>
+ <link field="reset_entries" reltype="has_many" key="hold" map="" class="ahrrre"/>
<link field="eligible_copies" reltype="has_many" key="hold" map="target_copy" class="ahcm"/>
<link field="bib_rec" reltype="might_have" key="id" map="" class="rhrr"/>
<link field="cancel_cause" reltype="might_have" key="id" map="" class="ahrcc"/>
<field reporter:label="Transaction Closed?" name="xact_closed" sr:suggest_filter="true" reporter:datatype="bool"/>
</fields>
</class>
+
+ <class id="ahrrr" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::hold_request_reset_reason" oils_persist:tablename="action.hold_request_reset_reason" reporter:label="Hold Request Reset Reason" oils_persist:field_safe="true">
+ <fields oils_persist:primary="id" oils_persist:sequence="action.hold_request_reset_reason_id_seq">
+ <field name="id" reporter:selector="name" reporter:datatype="id" reporter:label="ID"/>
+ <field name="name" reporter:datatype="text" oils_persist:i18n="true" reporter:label="Name"/>
+ <field name="manual" reporter:datatype="bool" reporter:label="Manual"/>
+ </fields>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
+ </class>
+
+ <class id="ahrrre" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::hold_request_reset_reason_entry" oils_persist:tablename="action.hold_request_reset_reason_entry" reporter:label="Hold Request Reset Reason Entry" oils_persist:field_safe="true">
+ <fields oils_persist:primary="id" oils_persist:sequence="action.hold_request_reset_reason_entry_id_seq">
+ <field name="id" reporter:datatype="id" reporter:label="ID"/>
+ <field name="hold" reporter:label="ID" reporter:datatype="link"/>
+ <field name="reset_reason" reporter:label="ID" reporter:datatype="link"/>
+ <field name="reset_time" reporter:datatype="timestamp" reporter:label="Reset Date/Time"/>
+ <field name="note" reporter:datatype="text" oils_persist:i18n="true" reporter:label="Note"/>
+ <field name="requestor" reporter:label="Resetting User" reporter:datatype="link"/>
+ <field name="requestor_workstation" reporter:label="Resetting User Workstation" reporter:datatype="link"/>
+ <field name="previous_copy" reporter:label="Previous Copy" reporter:datatype="link"/>
+ </fields>
+ <links>
+ <link field="hold" reltype="has_a" key="id" map="" class="ahr"/>
+ <link field="reset_reason" reltype="has_a" key="id" map="" class="ahrrr"/>
+ <link field="requestor" reltype="has_a" key="id" map="" class="au"/>
+ <link field="requestor_workstation" reltype="has_a" key="id" map="" class="aws"/>
+ <link field="previous_copy" reltype="has_a" key="id" map="" class="acp"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve permission="VIEW_HOLD">
+ <context link="pickup_lib" field="hold"/>
+ </retrieve>
+ </actions>
+ </permacrud>
+ </class>
+
<class id="coauf"
controller="open-ils.cstore open-ils.pcrud"
$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;
-
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!
+ 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_expire_time;
$hold->clear_cancel_time;
$hold->clear_prev_check_time unless $hold->prev_check_time;
-
+
+ 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');
- $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->clear_prev_check_time;
$hold->clear_shelf_expire_time;
$hold->clear_current_shelf_lib;
-
+ _create_reset_reason_entry($e,$hold,OILS_HOLD_UNCANCELED);
$e->update_action_hold_request($hold) or return $e->die_event;
$e->commit;
-
+
$U->simplereq('open-ils.hold-targeter',
'open-ils.hold-targeter.target', {hold => $hold_id});
-
return 1;
}
my $e = new_editor(authtoken=>$auth, xact=>1);
return $e->die_event unless $e->checkauth;
-
+
+ my $req = $e->requestor->id;
+
my $hold = $e->retrieve_action_hold_request($holdid)
or return $e->die_event;
- if( $e->requestor->id ne $hold->usr ) {
+ if( $req ne $hold->usr ) {
return $e->die_event unless $e->allowed('CANCEL_HOLDS');
}
$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 "";
+ _create_reset_reason_entry($e,$hold,OILS_HOLD_CANCELED,$note_body);
$e->update_action_hold_request($hold)
or return $e->die_event;
my($self, $e, $hold, $values) = @_;
my $hold_status;
my $need_retarget = 0;
+ my $reset_reason = OILS_HOLD_UPDATED;
+ my $note_body = "";
unless($hold) {
$hold = $e->retrieve_action_hold_request($values->{id})
if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
# Value changed? RETARGET!
$need_retarget = 1;
+ $reset_reason = OILS_HOLD_UPDATED;
+ $note_body .= "$k value changed."
} elsif (defined $hold->$k() != defined $values->{$k}) {
# Value being set or cleared? RETARGET!
$need_retarget = 1;
+ $reset_reason = OILS_HOLD_UPDATED;
+ $note_body .= "$k value cleared."
}
}
if (defined $values->{$k}) {
$hold->clear_current_copy;
}
}
-
+
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);
$hold->clear_current_copy;
$hold->clear_prev_check_time;
# Clear expire_time to prevent frozen holds from expiring.
}
$e->update_action_hold_request($hold) or return $e->die_event;
+ _create_reset_reason_entry($e,$hold,$reset_reason,$note_body) if $need_retarget;
$e->commit;
-
+
if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
$logger->info("Running targeter on activated hold ".$hold->id);
+ $U->simplereq('open-ils.circ',
+ 'open-ils.circ.hold_reset_reason_entry.create',$e->authtoken,$hold->id,OILS_HOLD_UNFROZEN);
$U->simplereq('open-ils.hold-targeter',
'open-ils.hold-targeter.target', {hold => $hold->id});
}
# a change to mint-condition changes the set of potential copies, so retarget the hold;
if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
_reset_hold($self, $e->requestor, $hold)
- } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
+ } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
$U->simplereq('open-ils.hold-targeter',
'open-ils.hold-targeter.target', {hold => $hold->id});
}
-
+
+
+
return $hold->id;
}
} else {
if($U->is_true($orig_hold->frozen)) {
$logger->info("Running targeter on activated hold ".$hold->id);
+ _create_reset_reason_entry($e,$hold,OILS_HOLD_UNFROZEN,"Running targeter on activated hold");
$U->simplereq('open-ils.hold-targeter',
'open-ils.hold-targeter.target', {hold => $hold->id});
}
return 1;
}
+__PACKAGE__->register_method(
+ method => 'create_reset_reason_entry',
+ api_name => 'open-ils.circ.hold_reset_reason_entry.create'
+);
+
+sub create_reset_reason_entry
+{
+ my($self, $conn, $auth, $hold, $reset_reason, $note, $previous_copy) = @_;
+ my $e = new_editor(authtoken => $auth, xact => 1);
+ #checkauth to set the requestor (if available)
+ $e->checkauth;
+ my @holds;
+ if(ref $hold eq 'ARRAY'){
+ @holds = @{$hold};
+ }
+ else{
+ @holds = ($hold);
+ }
+ for my $holdid (@holds){
+ try{
+ my ($hold, $evt) = $U->fetch_hold($holdid);
+ _create_reset_reason_entry(
+ $e,
+ $hold,
+ $reset_reason,
+ $note,
+ $previous_copy)
+ 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,$previous_copy) = @_;
+ return 1 unless _reset_reason_entry_flag();
+ 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 = defined $previous_copy ? $previous_copy : $hold->current_copy;
+ $entry->hold($hold->id);
+ $entry->reset_reason($reset_reason);
+ $entry->reset_time('now');
+ $entry->previous_copy($last_copy);
+ $entry->note($note) if defined $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;
+}
+
+sub _reset_reason_entry_flag
+{
+ my $do_ahrrre = $U->get_global_flag('circ.holds.create_reset_reason_entries');
+ return ($do_ahrrre and $U->is_true($do_ahrrre->enabled));
+}
__PACKAGE__->register_method(
method => 'reset_hold_batch',
my ($self, $reqr, $hold) = @_;
my $e = new_editor(xact =>1, requestor => $reqr);
-
- $logger->info("reseting hold ".$hold->id);
-
my $hid = $hold->id;
-
+ $logger->info("reseting hold ".$hid." requestor was ".$reqr->usrname." (ID ".$reqr->id.")");
+ my $note_body = "";
if( $hold->capture_time and $hold->current_copy ) {
my $copy = $e->retrieve_asset_copy($hold->current_copy)
if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
$logger->info("setting copy to status 'reshelving' on hold retarget");
+ $note_body.=" set copy to status 'reshelving'.";
$copy->status(OILS_COPY_STATUS_RESHELVING);
$copy->editor($e->requestor->id);
$copy->edit_date('now');
$logger->info("Aborting transit [$transid] on hold [$hid] reset...");
my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
$logger->info("Transit abort completed with result $evt");
+ $note_body.=" Transit abort completed with result $evt.";
unless ("$evt" eq 1) {
$e->rollback;
return $evt;
}
}
+ _create_reset_reason_entry($e,$hold,OILS_HOLD_MANUAL_RESET,$note_body);
$hold->clear_capture_time;
$hold->clear_current_copy;
$hold->clear_shelf_time;
$hold->clear_current_shelf_lib;
$e->update_action_hold_request($hold) or return $e->die_event;
+
$e->commit;
-
+
$U->simplereq('open-ils.hold-targeter',
'open-ils.hold-targeter.target', {hold => $hold->id});
next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
$logger->info("circulator: clearing current_copy and prev_check_time on hold ".
$old_hold->id." after a better hold [".$best_hold->id."] was found");
+ _create_reset_reason_entry($editor,$old_hold,OILS_HOLD_BETTER_HOLD,"Old hold was reset. Last check time was ".$old_hold->prev_check_time.". a better hold [".$best_hold->id."] was found");
$old_hold->clear_current_copy;
$old_hold->clear_prev_check_time;
$editor->update_action_hold_request($old_hold)
use OpenILS::Application;
use base qw/OpenILS::Application/;
use OpenILS::Utils::HoldTargeter;
+use OpenILS::Const qw/:const/;
use OpenSRF::Utils::Logger qw(:logger);
+use OpenSRF::EX qw(:try);
__PACKAGE__->register_method(
method => 'hold_targeter',
my $total = scalar(@hold_ids);
$logger->info("targeter processing $total holds");
-
+ my $hold_ses = create OpenSRF::AppSession("open-ils.circ");
+
for my $hold_id (@hold_ids) {
$count++;
$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,
+ undef,
+ $single->{previous_copy_id}
+ ) unless defined
+ $args->{hold} ||
+ $single->{previous_copy_id} == $single->hold->current_copy;
+ } 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
$client->respond($res);
$logger->info("targeted $count of $total holds");
- }
+ }
}
-
+ $hold_ses->disconnect;
+
return undef;
}
econst OILS_BILLING_TYPE_NOTIFICATION_FEE => 9;
+# ---------------------------------------------------------------------
+# Hold reset reasons
+# ---------------------------------------------------------------------
+econst OILS_HOLD_TIMED_OUT => 1;
+econst OILS_HOLD_MANUAL_RESET => 2;
+econst OILS_HOLD_BETTER_HOLD => 3;
+econst OILS_HOLD_FROZEN => 4;
+econst OILS_HOLD_UNFROZEN => 5;
+econst OILS_HOLD_CANCELED => 6;
+econst OILS_HOLD_UNCANCELED => 7;
+econst OILS_HOLD_UPDATED => 8;
+econst OILS_HOLD_CHECK_OUT => 9;
+econst OILS_HOLD_CHECK_IN => 10;
# ---------------------------------------------------------------------
# Non Evergreen-specific constants
--- /dev/null
+#!perl
+use strict;
+use warnings;
+
+use Test::More tests => 23;
+diag("Hold Reset Reason Tests");
+
+use OpenILS::Const qw/:const/;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+
+my $script = OpenILS::Utils::TestUtils->new();
+my $U = 'OpenILS::Application::AppUtils';
+my $e = new_editor();
+use constant {
+ BR1_WORKSTATION => 'BR1-test-lp2012669-hold-reset-reasons.t',
+ BR1_ID => 4,
+ BR2_ID => 5,
+ HOLD_ID => 67,
+ CANCEL_CAUSE => '5',
+ CANCEL_NOTE => 'TEST NOTE',
+ REQ_ID => 1,
+};
+
+$script->bootstrap;
+$e->init;
+
+# Login as admin at BR1.
+my $authtoken = $script->authenticate({
+ username=>'admin',
+ password=>'demo123',
+ type=>'staff'
+});
+ok(
+ $script->authtoken,
+ 'Have an authtoken'
+);
+
+# Register workstation.
+my $ws = $script->find_or_register_workstation(BR1_WORKSTATION, BR1_ID);
+ok(
+ ! ref $ws,
+ 'Found or registered workstation'
+);
+
+# Logout.
+$script->logout();
+ok(
+ ! $script->authtoken,
+ 'Successfully logged out'
+);
+
+# Login as admin at BR1 using the workstation.
+$authtoken = $script->authenticate({
+ username=>'admin',
+ password=>'demo123',
+ type=>'staff',
+ workstation => BR1_WORKSTATION
+});
+ok(
+ $script->authtoken,
+ 'Have an authtoken'
+);
+
+# == Reseting Concerto hold 67.
+
+$U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.reset',
+ $authtoken,
+ HOLD_ID
+);
+
+my $ahrrre = $e->search_action_hold_request_reset_reason_entry([{hold => HOLD_ID},{order_by =>[{class => 'ahrrre', field=> 'reset_time', direction => 'DESC'}]}])->[0];
+is($ahrrre->reset_reason, OILS_HOLD_MANUAL_RESET,"manual reset code applied to reset reason");
+is($ahrrre->requestor, REQ_ID,"requestor ID applied to reset reason");
+is($ahrrre->requestor_workstation, $ws,"workstation applied to reset reason");
+
+$U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.cancel',
+ $authtoken,
+ HOLD_ID,
+ CANCEL_CAUSE
+);
+
+$ahrrre = $e->search_action_hold_request_reset_reason_entry([{hold => HOLD_ID},{order_by =>[{class => 'ahrrre', field=> 'reset_time', direction => 'DESC'}]}])->[0];
+is($ahrrre->reset_reason, OILS_HOLD_CANCELED,"cancel code applied to reset reason");
+is($ahrrre->requestor, REQ_ID,"requestor ID applied to reset reason");
+is($ahrrre->requestor_workstation, $ws,"workstation applied to reset reason");
+is($ahrrre->note, "Cancel Cause: Staff forced","cancel cause appended to reset reason's note");
+
+$U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.uncancel',
+ $authtoken,
+ HOLD_ID
+);
+
+$ahrrre = $e->search_action_hold_request_reset_reason_entry([{hold => HOLD_ID},{order_by =>[{class => 'ahrrre', field=> 'reset_time', direction => 'DESC'}]}])->[0];
+is($ahrrre->reset_reason, OILS_HOLD_UNCANCELED,"cancel code applied to reset reason");
+is($ahrrre->requestor, REQ_ID,"requestor ID applied to reset reason");
+is($ahrrre->requestor_workstation, $ws,"workstation applied to reset reason");
+
+$U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.update',
+ $authtoken,
+ undef,
+ {id => HOLD_ID, frozen => 1}
+);
+
+$ahrrre = $e->search_action_hold_request_reset_reason_entry([{hold => HOLD_ID},{order_by =>[{class => 'ahrrre', field=> 'reset_time', direction => 'DESC'}]}])->[0];
+is($ahrrre->reset_reason, OILS_HOLD_FROZEN,"frozen code applied to reset reason");
+is($ahrrre->requestor, REQ_ID,"requestor ID applied to reset reason");
+is($ahrrre->requestor_workstation, $ws,"workstation applied to reset reason");
+
+$U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.update',
+ $authtoken,
+ undef,
+ {id => HOLD_ID, frozen => 0}
+);
+
+$ahrrre = $e->search_action_hold_request_reset_reason_entry([{hold => HOLD_ID},{order_by =>[{class => 'ahrrre', field=> 'reset_time', direction => 'DESC'}]}])->[0];
+is($ahrrre->reset_reason, OILS_HOLD_UNFROZEN,"unfrozen code applied to reset reason");
+is($ahrrre->requestor, REQ_ID,"requestor ID applied to reset reason");
+is($ahrrre->requestor_workstation, $ws,"workstation applied to reset reason");
+
+$U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.update',
+ $authtoken,
+ undef,
+ {id => HOLD_ID, pickup_lib => BR2_ID}
+);
+
+$ahrrre = $e->search_action_hold_request_reset_reason_entry([{hold => HOLD_ID},{order_by =>[{class => 'ahrrre', field=> 'reset_time', direction => 'DESC'}]}])->[0];
+is($ahrrre->reset_reason, OILS_HOLD_UPDATED,"hold updated code applied to reset reason");
+is($ahrrre->requestor, REQ_ID,"requestor ID applied to reset reason");
+is($ahrrre->requestor_workstation, $ws,"workstation applied to reset reason");
+
+$U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.update',
+ $authtoken,
+ undef,
+ {id => HOLD_ID, pickup_lib => BR1_ID}
+);
+
+my $ahrrre_list = $e->search_action_hold_request_reset_reason_entry([{hold => HOLD_ID},{order_by =>[{class => 'ahrrre', field=> 'reset_time', direction => 'DESC'}]}]);
+
+#clean up all reset reasons
+$e->xact_begin;
+if ($ahrrre_list) {
+ for my $rr (@$ahrrre_list) {
+ next unless $rr;
+ $e->delete_action_hold_request_reset_reason_entry($rr);
+ }
+}
+$e->xact_commit;
\ No newline at end of file
hold INT NOT NULL REFERENCES action.hold_request (id) ON UPDATE CASCADE ON DELETE CASCADE
);
+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)
+);
+
+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
+);
+
+CREATE INDEX ahrrre_hold ON action.hold_request_reset_reason_entry (hold);
+
COMMIT;
'cwst', 'label'
)
);
+
+-- Hold reset reasons
+
+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);
+
+INSERT INTO config.global_flag (name, label, enabled)
+VALUES (
+ 'circ.holds.create_reset_reason_entries',
+ oils_i18n_gettext(
+ 'circ.holds.create_reset_reason_entries',
+ 'Create reset reasons whenever a hold has been reset.',
+ 'cgf',
+ 'label'
+ ),
+ TRUE
+);
\ No newline at end of file
--- /dev/null
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('xxxx', :eg_version);
+
+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)
+);
+
+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
+);
+
+CREATE INDEX ahrrre_hold ON action.hold_request_reset_reason_entry (hold);
+
+INSERT INTO config.global_flag (name, label, enabled)
+ VALUES (
+ 'circ.holds.create_reset_reason_entries',
+ oils_i18n_gettext(
+ 'circ.holds.create_reset_reason_entries',
+ 'Create reset reasons whenever a hold has been reset.',
+ 'cgf',
+ 'label'
+ ),
+ TRUE
+ );
+
+COMMIT;
\ No newline at end of file
[% l('Staff Notifications') %]
</a>
</li>
+ <li ng-class="{active : detail_tab == 'resets'}">
+ <a href ng-click="show_resets_tab()">
+ [% l('Reset Entries') %]
+ </a>
+ </li>
</ul>
<div class="tab-content">
<div class="tab-pane active">
</div>
</div>
</div><!-- notes tab content -->
+
+<div ng-if="detail_tab == 'resets'">
+ <div class="btn-group column-picker">
+ <!-- change order -->
+ <button type="button" class="btn btn-default"
+ ng-click="reverse_reset_order()"
+ title="[% l('Reverse Order') %]">
+ <span class="glyphicon {{reverseResetOrder ? 'glyphicon-chevron-up' : 'glyphicon-chevron-down' }}"></span>
+ </button>
+
+ <!-- first page -->
+ <button type="button" class="btn btn-default"
+
+ ng-class="{disabled : on_first_rs_page()}"
+ ng-disabled="on_first_rs_page()"
+ ng-click="first_rs_page()"
+ title="[% l('Start') %]">
+ <span class="glyphicon glyphicon-fast-backward"></span>
+ </button>
+
+ <!-- previous page -->
+ <button type="button" class="btn btn-default"
+
+ ng-class="{disabled : on_first_rs_page()}"
+ ng-disabled="on_first_rs_page()"
+ ng-click="decrement_rs_page()"
+ title="[% l('Previous Page') %]">
+ <span class="glyphicon glyphicon-backward"></span>
+ </button>
+
+ <!-- next page -->
+ <!-- todo: paging needs a total count value to be fully functional -->
+ <button type="button" class="btn btn-default"
+
+ ng-class="{disabled : !has_next_rs_page()}"
+ ng-disabled="!has_next_rs_page()"
+ ng-click="increment_rs_page()"
+ title="[% l('Next Page') %]">
+ <span class="glyphicon glyphicon-forward"></span>
+ </button>
+ </div>
+ <div ng-show="!resetsLoaded" style="text-align:center;">
+ <img src='[% ctx.media_prefix %]/opac/images/progressbar_green.gif[% ctx.cache_key %]' style='height:32px;width:32px;' alt='[% l("Search In Progress") %]'/>
+ </div>
+ <div class="flex-container-striped flex-container-bordered" ng-show="resetsLoaded && filteredResets.length">
+ <div class="flex-row">
+ <div class="flex-cell strong-text">Time</div>
+ <div class="flex-cell strong-text">Reason</div>
+ <div class="flex-cell strong-text">Requestor</div>
+ <div class="flex-cell strong-text">Note</div>
+ <div class="flex-cell strong-text">Previous Copy</div>
+ </div>
+ <div class="flex-row" ng-repeat="reset in filteredResets">
+ <div class="flex-cell">{{reset.reset_time() | date:$root.egDateAndTimeFormat}}</div>
+ <div class="flex-cell">{{reset.reset_reason().name()}}</div>
+ <div class="flex-cell">{{reset.requestor().usrname()}}</div>
+ <div class="flex-cell">{{reset.note()}}</div>
+ <div class="flex-cell">
+ <a href="./cat/item/{{reset.previous_copy().id()}}" target="_self">
+ {{reset.previous_copy().barcode()}}
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="flex-container-striped flex-container-bordered" ng-show="resetsLoaded && !filteredResets.length">
+ <div class="flex-row">
+ [%- l('No reset entries found for this hold.') -%]
+ </div>
+ </div>
+ </div><!-- resets tab content -->
</div><!-- tab pane -->
</div><!-- tab-content -->
});
}
+
+ $scope.resetPage = 1;
+ $scope.resetsPerPage = 10;
+ $scope.maximumPages = 25;
+ $scope.resetsLoaded = false;
+ $scope.reverseResetOrder = false;
+
+ $scope.show_resets_tab = function() {
+ $scope.detail_tab = 'resets';
+ egCore.pcrud.search('ahrrre',
+ {hold : $scope.hold.id()},
+ {
+ flesh : 1,
+ flesh_fields : {ahrrre : ['reset_reason','requestor','previous_copy']},
+ limit : $scope.resetsPerPage * $scope.maximumPages
+ },
+ {atomic : true}
+ ).then(function(ents) {
+ // sort the reset notes by date
+ ents.sort(
+ function(a,b){
+ return Date.parse(a.reset_time()) - Date.parse(b.reset_time());
+ }
+ );
+ $scope.hold.reset_entries(ents);
+ $scope.filter_resets();
+ $scope.resetsLoaded = true;
+ });
+ }
+
+ $scope.filter_resets = function(){
+ if(
+ typeof($scope.hold) === 'undefined' ||
+ typeof($scope.hold.reset_entries) === 'undefined' ||
+ $scope.hold.reset_entries() === null
+ )
+ return;
+ var begin = (($scope.resetPage - 1) * $scope.resetsPerPage),
+ end = begin + $scope.resetsPerPage;
+ $scope.filteredResets = $scope.hold
+ .reset_entries()
+ .slice(begin,end);
+ }
+
+ $scope.reverse_reset_order = function(){
+ $scope.hold.reset_entries().reverse()
+ $scope.reverseResetOrder = !$scope.reverseResetOrder;
+ $scope.first_rs_page();
+ }
+
+ $scope.on_first_rs_page = function(){
+ return $scope.resetPage == 1;
+ }
+
+ $scope.has_next_rs_page = function(){
+ return $scope.resetPage < $scope.max_rs_pages();
+ }
+
+ $scope.max_rs_pages = function(){
+ if(typeof($scope.hold.reset_entries) === 'undefined' || $scope.hold.reset_entries() === null)
+ return 0;
+ return $scope.hold.reset_entries().length/$scope.resetsPerPage;
+ }
+
+ $scope.first_rs_page = function() {
+ $scope.resetPage = 1;
+ }
+
+ $scope.increment_rs_page = function() {
+ $scope.resetPage++;
+ }
+ $scope.decrement_rs_page = function() {
+ $scope.resetPage--;
+ }
+
+ $scope.$watch('resetPage',$scope.filter_resets);
+ $scope.$watch('reverseResetOrder',$scope.filter_resets);
+
$scope.show_notify_tab = function() {
$scope.detail_tab = 'notify';
egCore.pcrud.search('ahn',
====================
If the patron is not part of a hold group that is patron visible,
the *Hold Groups* option won't appear in their OPAC account menu.
-====================
\ No newline at end of file
+====================
+
+[[hold_reset_reasons]]
+== Hold Reset Reasons
+
+indexterm:[Reset, Retarget, Reset Reason]
+
+Hold reset reasons allow staff to see when and why a hold request has been reset. Reset reasons are generated any time a hold has been reset, whether that's a manual reset from a staff member or automatically because a hold has reached the hold retarget interval. This can be very useful for debugging the hold targeter or identifying bad actors in the system.
+
+=== Types of Reset Reasons
+There are ten different types of reset reasons that can be identified.
+
+. HOLD_TIMED_OUT
+. HOLD_MANUAL_RESET
+. HOLD_BETTER_HOLD
+. HOLD_FROZEN
+. HOLD_UNFROZEN
+. HOLD_CANCELED
+. HOLD_UNCANCELED
+. HOLD_UPDATED
+. HOLD_CHECKED_OUT
+. HOLD_CHECKED_IN
+
+=== Enabling Reset Reasons
+This feature is controlled by a global config flag: *circ.holds.create_reset_reason_entries*. Reset reasons will not be generated unless this is set to true.
+
+=== Viewing Reset Reasons
+Staff can view reset reasons for a hold via a patron's *holds* tab.
+
+. Open patron's page.
+. Click *holds* tab.
+. Select a hold to investigate.
+. Click *detail view*.
+. Click *Reset Entries*
+. Order can be reversed to show most recent resets first.