From: Llewellyn Marshall Date: Wed, 18 Aug 2021 17:13:47 +0000 (-0400) Subject: creates "reset reason entries" whenever a hold has been reset. Records X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=3c690bb150f6b3d6316dff74f45fd8fd69f1e806;p=working%2FEvergreen.git creates "reset reason entries" whenever a hold has been reset. Records previous copy, requesting user/ws, reset time and reset reason. reset reasons are stored in their own table and referenced in the perl constants file. Hold reset reason entries can be inspected in the staff client by viewing the "hold details" within a patron's profile. Includes adocs and an automated test for manual reset reasons. signed-off-by: Llewellyn Marshall --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 6c517fa0d0..fce878e9e6 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -6820,6 +6820,7 @@ SELECT usr, + @@ -6855,6 +6856,7 @@ SELECT usr, + @@ -15510,6 +15512,47 @@ SELECT usr, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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); @@ -2713,6 +2721,13 @@ sub checkin_retarget { 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', @@ -3369,7 +3384,14 @@ sub attempt_checkin_hold_capture { $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); @@ -3513,7 +3535,14 @@ sub retarget_holds { 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; } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm index 6e9878be21..2240083517 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm @@ -1038,13 +1038,12 @@ sub uncancel_hold { $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; } @@ -1072,11 +1071,13 @@ sub cancel_hold { 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'); } @@ -1120,6 +1121,21 @@ sub cancel_hold { $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; @@ -1220,6 +1236,8 @@ sub update_hold_impl { 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}) @@ -1231,9 +1249,13 @@ sub update_hold_impl { 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}) { @@ -1339,9 +1361,10 @@ sub update_hold_impl { $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. @@ -1363,10 +1386,13 @@ sub update_hold_impl { } $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}); } @@ -1374,11 +1400,13 @@ sub update_hold_impl { # 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; } @@ -1464,6 +1492,7 @@ sub update_hold_if_frozen { } 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}); } @@ -2223,6 +2252,67 @@ sub reset_hold { 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', @@ -2253,11 +2343,9 @@ sub _reset_hold { 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) @@ -2265,6 +2353,7 @@ sub _reset_hold { 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'); @@ -2281,6 +2370,7 @@ sub _reset_hold { $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; @@ -2290,6 +2380,7 @@ sub _reset_hold { } } + _create_reset_reason_entry($e,$hold,OILS_HOLD_MANUAL_RESET,$note_body); $hold->clear_capture_time; $hold->clear_current_copy; $hold->clear_shelf_time; @@ -2297,8 +2388,9 @@ sub _reset_hold { $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}); @@ -3509,6 +3601,7 @@ sub find_nearest_permitted_hold { 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) diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/HoldTargeter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/HoldTargeter.pm index afca2fcc9e..ad7ed5b88c 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/HoldTargeter.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/HoldTargeter.pm @@ -4,7 +4,9 @@ use warnings; 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', @@ -80,7 +82,8 @@ sub 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++; @@ -96,6 +99,27 @@ sub hold_targeter { $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 @@ -105,9 +129,10 @@ sub hold_targeter { $client->respond($res); $logger->info("targeted $count of $total holds"); - } + } } - + $hold_ses->disconnect; + return undef; } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Const.pm b/Open-ILS/src/perlmods/lib/OpenILS/Const.pm index 0ff488090f..093f1a67ea 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Const.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Const.pm @@ -133,6 +133,19 @@ econst OILS_PENALTY_INVALID_PATRON_ADDRESS => 29; 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 diff --git a/Open-ILS/src/perlmods/live_t/lp2012669-hold-reset-reasons.t b/Open-ILS/src/perlmods/live_t/lp2012669-hold-reset-reasons.t new file mode 100644 index 0000000000..6335dd9b52 --- /dev/null +++ b/Open-ILS/src/perlmods/live_t/lp2012669-hold-reset-reasons.t @@ -0,0 +1,163 @@ +#!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 diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql index 711269e245..d193b61be3 100644 --- a/Open-ILS/src/sql/Pg/090.schema.action.sql +++ b/Open-ILS/src/sql/Pg/090.schema.action.sql @@ -1796,5 +1796,44 @@ CREATE TABLE action.batch_hold_event_map ( 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; diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 35b2765639..88df9e37f2 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -23222,3 +23222,29 @@ VALUES ( '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 diff --git a/Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql new file mode 100644 index 0000000000..42672af91d --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql @@ -0,0 +1,68 @@ +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 diff --git a/Open-ILS/src/templates/staff/circ/share/t_hold_details.tt2 b/Open-ILS/src/templates/staff/circ/share/t_hold_details.tt2 index ece4719944..7da498ac2d 100644 --- a/Open-ILS/src/templates/staff/circ/share/t_hold_details.tt2 +++ b/Open-ILS/src/templates/staff/circ/share/t_hold_details.tt2 @@ -71,6 +71,11 @@ [% l('Staff Notifications') %] +
  • + + [% l('Reset Entries') %] + +
  • @@ -152,6 +157,76 @@
    + +
    +
    + + + + + + + + + + + + +
    +
    + [% l("Search In Progress") %] +
    +
    +
    +
    Time
    +
    Reason
    +
    Requestor
    +
    Note
    +
    Previous Copy
    +
    +
    +
    {{reset.reset_time() | date:$root.egDateAndTimeFormat}}
    +
    {{reset.reset_reason().name()}}
    +
    {{reset.requestor().usrname()}}
    +
    {{reset.note()}}
    + +
    +
    +
    +
    + [%- l('No reset entries found for this hold.') -%] +
    +
    +
    diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/holds.js b/Open-ILS/web/js/ui/default/staff/circ/services/holds.js index 251ceb216e..6779376526 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/services/holds.js +++ b/Open-ILS/web/js/ui/default/staff/circ/services/holds.js @@ -858,7 +858,85 @@ function($window , $location , $timeout , egCore , egHolds , egCirc) { }); } + + $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', diff --git a/docs/modules/circulation/pages/basic_holds.adoc b/docs/modules/circulation/pages/basic_holds.adoc index 78e943d573..a73c0eab2f 100644 --- a/docs/modules/circulation/pages/basic_holds.adoc +++ b/docs/modules/circulation/pages/basic_holds.adoc @@ -811,4 +811,38 @@ image::hold_management/hold_groups_menu_opac.jpg[Hold Group Option in OPAC] ==================== 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.