LP 1779467: Enhance Mark Items Functionality
authorJason Stephenson <jason@sigio.com>
Sat, 27 Oct 2018 19:48:54 +0000 (15:48 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Wed, 31 Jul 2019 15:31:48 +0000 (11:31 -0400)
A new option to "Mark Item as Discard/Weed" is added to many actions
menus in the staff client.  This command is connected to the back end
function open-ils.circ.mark_item_discard.

The back end functionality for the open-ils.circ.mark_item_* family of
functions is altered to provide more consistent behavior and to avoid
some strange situations that have come up in the past, such as items
with the Missing status having active transits or open circulations.

The code for "Mark Item as Damaged" and "Mark Item as Missing" are
altered to take advantage of the back end changes.  NB: These changes
do not affect the "Mark Item as Missing Pieces" function, as that is
handled by different back end code.

Perl live tests are added for the backend functionality changes to
test that certain conditions works.  Like most of our tests these
could be expanded to cover more potential situations.

See the release notes for more detail on changes in functionality.

Signed-off-by: Jason Stephenson <jason@sigio.com>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>
21 files changed:
Open-ILS/src/extras/ils_events.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Open-ILS/src/perlmods/live_t/zz-lp1779467-mark-item-discard.t [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
Open-ILS/src/templates/staff/cat/catalog/t_holds.tt2
Open-ILS/src/templates/staff/cat/item/index.tt2
Open-ILS/src/templates/staff/cat/item/t_list.tt2
Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2
Open-ILS/src/templates/staff/circ/holds/t_pull_list.tt2
Open-ILS/src/templates/staff/circ/holds/t_shelf_list.tt2
Open-ILS/src/templates/staff/circ/patron/t_holds_list.tt2
Open-ILS/src/templates/staff/circ/renew/t_renew.tt2
Open-ILS/src/templates/staff/circ/share/circ_strings.tt2
Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
Open-ILS/web/js/ui/default/staff/cat/item/app.js
Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
Open-ILS/web/js/ui/default/staff/circ/renew/app.js
Open-ILS/web/js/ui/default/staff/circ/services/circ.js
Open-ILS/web/js/ui/default/staff/circ/services/holds.js
Open-ILS/web/js/ui/default/staff/circ/services/item.js
docs/RELEASE_NOTES_NEXT/Circulation/enhanced-mark-item-functionality.adoc [new file with mode: 0644]

index 9f92944..362fff5 100644 (file)
     <event code='11010' textcode='SERIAL_CAPTION_AND_PATTERN_NOT_EMPTY'>
         <desc xml:lang="en-US">The prediction pattern still has dependent objects</desc>
     </event>
+
+       <!-- ================================================================ -->
+
+    <event code='12000' textcode='ITEM_TO_MARK_CHECKED_OUT'>
+      <desc xml:lang="en-US">The item to be marked is checked out to a patron.</desc>
+    </event>
+    <event code='12001' textcode='ITEM_TO_MARK_IN_TRANSIT'>
+      <desc xml:lang="en-US">The item to be marked is in transit.</desc>
+    </event>
+    <event code='12002' textcode='ITEM_TO_MARK_LAST_HOLD_COPY'>
+      <desc xml:lang="en-US">The item to be marked is the last possible target for a hold.</desc>
+    </event>
 </ils_events>
 
 
index bad3be3..f3cec22 100644 (file)
@@ -1307,31 +1307,24 @@ sub mark_item {
     my( $self, $conn, $auth, $copy_id, $args ) = @_;
     $args ||= {};
 
-    # Items must be checked in before any attempt is made to mark damaged
-    my $evt = try_checkin($auth, $copy_id) if
-        ($self->api_name=~ /damaged/ && $args->{handle_checkin});
-    return $evt if $evt;
-
-    my $e = new_editor(authtoken=>$auth, xact =>1);
+    my $e = new_editor(authtoken=>$auth);
     return $e->die_event unless $e->checkauth;
     my $copy = $e->retrieve_asset_copy([
         $copy_id,
-        {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
+        {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
             or return $e->die_event;
 
-    my $owning_lib = 
+    my $owning_lib =
         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
             $copy->circ_lib : $copy->call_number->owning_lib;
 
+    my $evt; # For later.
     my $perm = 'MARK_ITEM_MISSING';
     my $stat = OILS_COPY_STATUS_MISSING;
 
     if( $self->api_name =~ /damaged/ ) {
         $perm = 'MARK_ITEM_DAMAGED';
         $stat = OILS_COPY_STATUS_DAMAGED;
-        my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
-        return $evt if $evt;
-
     } elsif ( $self->api_name =~ /bindery/ ) {
         $perm = 'MARK_ITEM_BINDERY';
         $stat = OILS_COPY_STATUS_BINDERY;
@@ -1355,20 +1348,73 @@ sub mark_item {
     # caller may proceed if either perm is allowed
     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
 
-    $copy->status($stat);
-    $copy->edit_date('now');
-    $copy->editor($e->requestor->id);
-
-    $e->update_asset_copy($copy) or return $e->die_event;
+    # Copy status checks.
+    if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
+        # Items must be checked in before any attempt is made to change its status.
+        if ($args->{handle_checkin}) {
+            $evt = try_checkin($auth, $copy_id);
+        } else {
+            $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
+        }
+    } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
+        # Items in transit need to have the transit aborted before being marked.
+        if ($args->{handle_transit}) {
+            $evt = try_abort_transit($auth, $copy_id);
+        } else {
+            $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
+        }
+    } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
+        # Items with restrict_copy_delete status require the
+        # COPY_DELETE_WARNING.override permission to be marked for
+        # discard.
+        if ($args->{handle_copy_delete_warning}) {
+            $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
+        } else {
+            $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
+        }
+    }
+    return $evt if $evt;
 
+    # Retrieving holds for later use.
     my $holds = $e->search_action_hold_request(
-        { 
+        {
             current_copy => $copy->id,
             fulfillment_time => undef,
             cancel_time => undef,
-        }
+        },
+        {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
     );
 
+    # Throw event if attempting to  mark discard the only copy to fill a hold.
+    if ($self->api_name =~ /discard/) {
+        if (!$args->{handle_last_hold_copy}) {
+            for my $hold (@$holds) {
+                my $eligible = $hold->eligible_copies();
+                if (scalar(@{$eligible}) < 2) {
+                    $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
+                    last;
+                }
+            }
+        }
+    }
+    return $evt if $evt;
+
+    # Things below here require a transaction and there is nothing left to interfere with it.
+    $e->xact_begin;
+
+    # Handle extra mark damaged charges, etc.
+    if ($self->api_name =~ /damaged/) {
+        $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
+        return $evt if $evt;
+    }
+
+    # Mark the copy.
+    $copy->status($stat);
+    $copy->edit_date('now');
+    $copy->editor($e->requestor->id);
+
+    $e->update_asset_copy($copy) or return $e->die_event;
+
     $e->commit;
 
     if( $self->api_name =~ /damaged/ ) {
@@ -1408,6 +1454,19 @@ sub try_checkin {
     }
 }
 
+sub try_abort_transit {
+    my ($auth, $copy_id) = @_;
+
+    my $abort = $U->simplereq(
+        'open-ils.circ',
+        'open-ils.circ.transit.abort',
+        $auth, {copyid => $copy_id}
+    );
+    # Above returns 1 or an event.
+    return $abort if (ref $abort);
+    return undef;
+}
+
 sub handle_mark_damaged {
     my($e, $copy, $owning_lib, $args) = @_;
 
diff --git a/Open-ILS/src/perlmods/live_t/zz-lp1779467-mark-item-discard.t b/Open-ILS/src/perlmods/live_t/zz-lp1779467-mark-item-discard.t
new file mode 100644 (file)
index 0000000..08f99b1
--- /dev/null
@@ -0,0 +1,238 @@
+#!perl
+use strict; use warnings;
+use Test::More tests => 17;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Const qw(:const);
+
+my $script = OpenILS::Utils::TestUtils->new();
+my $U = 'OpenILS::Application::AppUtils';
+
+diag("Test LP 1779467 Enhance Mark Item Discard/Weed.");
+
+use constant {
+    BR1_ID => 4,
+    BR3_ID => 6,
+    WORKSTATION => 'BR1-lp1779467-test-mark-item-discard'
+};
+
+# We are deliberately NOT using the admin user to check for a perm failure.
+my $credentials = {
+    username => 'br1mtownsend',
+    password => 'maryt1234',
+    type => 'staff'
+};
+
+# Log in as staff.
+my $authtoken = $script->authenticate($credentials);
+ok(
+    $authtoken,
+    'Logged in'
+) or BAIL_OUT('Must log in');
+
+# Find or register workstation.
+my $ws = $script->find_or_register_workstation(WORKSTATION, BR1_ID);
+ok(
+    ! ref $ws,
+    'Found or registered workstation'
+) or BAIL_OUT('Need Workstation');
+
+# Logout.
+$script->logout();
+ok(
+    ! $script->authtoken,
+    'Logged out'
+);
+
+# Login with workstation.
+$credentials->{workstation} = WORKSTATION;
+$credentials->{password} = 'maryt1234';
+$authtoken = $script->authenticate($credentials);
+ok(
+    $script->authtoken,
+    'Logged in with workstation'
+) or BAIL_OUT('Must log in');
+
+# Find available copy at BR1
+my $acps = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.search.acp.atomic',
+    $authtoken,
+    {circ_lib => BR1_ID, status => OILS_COPY_STATUS_AVAILABLE},
+    {limit => 1}
+);
+my $copy = $acps->[0];
+isa_ok(
+    ref $copy,
+    'Fieldmapper::asset::copy',
+    'Got available copy from BR1'
+);
+
+# Mark it discard/weed.
+my $result = $U->simplereq(
+    'open-ils.circ',
+    'open-ils.circ.mark_item_discard',
+    $authtoken,
+    $copy->id()
+);
+is(
+    $result,
+    1,
+    'Mark available copy Discard/Weed'
+);
+
+# Check its status.
+$copy = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.retrieve.acp',
+    $authtoken,
+    $copy->id()
+);
+is(
+    $copy->status(),
+    OILS_COPY_STATUS_DISCARD,
+    'Copy has Discard/Weed status'
+);
+
+# Find available copy at BR3.
+$acps = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.search.acp.atomic',
+    $authtoken,
+    {circ_lib => BR3_ID, status => OILS_COPY_STATUS_AVAILABLE},
+    {limit => 1}
+);
+$copy = $acps->[0];
+isa_ok(
+    ref $copy,
+    'Fieldmapper::asset::copy',
+    'Got available copy from BR3'
+);
+
+# Attempt to mark it discard/weed.
+# Should fail with a perm error.
+$result = $U->simplereq(
+    'open-ils.circ',
+    'open-ils.circ.mark_item_discard',
+    $authtoken,
+    $copy->id()
+);
+is(
+    $result->{textcode},
+    'PERM_FAILURE',
+    'Mark BR3 copy Discard/Weed'
+);
+
+# Find checked out copy at BR1.
+$acps = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.search.acp.atomic',
+    $authtoken,
+    {circ_lib => BR1_ID, status => OILS_COPY_STATUS_CHECKED_OUT},
+    {limit => 1}
+);
+$copy = $acps->[0];
+isa_ok(
+    ref $copy,
+    'Fieldmapper::asset::copy',
+    'Got checked out copy from BR1'
+);
+
+# Mark it discard/weed with handle_checkin: 1.
+$result = $U->simplereq(
+    'open-ils.circ',
+    'open-ils.circ.mark_item_discard',
+    $authtoken,
+    $copy->id(),
+    {handle_checkin => 1}
+);
+ok(
+    $result == 1,
+    'Mark checked out item discard'
+);
+
+# Check its status.
+$copy = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.retrieve.acp',
+    $authtoken,
+    $copy->id()
+);
+is(
+    $copy->status(),
+    OILS_COPY_STATUS_DISCARD,
+    'Checked out copy has Discard/Weed status'
+);
+
+# Check that it is no longer checked out.
+my $circ = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.search.circ',
+    $authtoken,
+    {target_copy => $copy->id(), checkin_time => undef}
+);
+ok(
+    ! defined $circ,
+    'No circulation for marked copy'
+);
+
+# Find another checked out copy at BR1.
+$acps = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.search.acp.atomic',
+    $authtoken,
+    {circ_lib => BR1_ID, status => OILS_COPY_STATUS_CHECKED_OUT},
+    {limit => 1}
+);
+$copy = $acps->[0];
+isa_ok(
+    ref $copy,
+    'Fieldmapper::asset::copy',
+    'Got another checked out copy from BR1'
+);
+
+# Mark it discard/weed with handle_checkin: 0.
+$result = $U->simplereq(
+    'open-ils.circ',
+    'open-ils.circ.mark_item_discard',
+    $authtoken,
+    $copy->id(),
+    {handle_checkin => 0}
+);
+# Check that we got the appropriate event: ITEM_TO_MARK_CHECKED_OUT
+is(
+    $result->{textcode},
+    'ITEM_TO_MARK_CHECKED_OUT',
+    'Mark second checked out item discard'
+);
+
+# Check its status.
+$copy = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.retrieve.acp',
+    $authtoken,
+    $copy->id()
+);
+is(
+    $copy->status(),
+    OILS_COPY_STATUS_CHECKED_OUT,
+    'Second checked out copy has Checked Out status'
+);
+
+# Check that it is still checked out.
+$circ = $U->simplereq(
+    'open-ils.pcrud',
+    'open-ils.pcrud.search.circ',
+    $authtoken,
+    {target_copy => $copy->id(), checkin_time => undef}
+);
+isa_ok(
+    $circ,
+    'Fieldmapper::action::circulation',
+    'Second copy still has a circulation'
+);
+
+# We could add more tests for other conditions, i.e. a copy in transit
+# and for marking other statuses.
+
+# Logout
+$script->logout(); # Not a test, just to be pedantic.
index b694d6b..d30ad1a 100644 (file)
@@ -62,6 +62,8 @@
 
     <eg-grid-action handler="selectedHoldingsDamaged" group="[% l('Mark') %]"
       label="[% l('Item as Damaged') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsDiscard" group="[% l('Mark') %]"
+      label="[% l('Item as Discard/Weed') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsMissing" group="[% l('Mark') %]"
       label="[% l('Item as Missing') %]"></eg-grid-action>
     <eg-grid-action handler="markFromSelectedAsHoldingsTarget" group="[% l('Mark') %]"
index c39953f..f50be4c 100644 (file)
@@ -49,6 +49,8 @@
       label="[% l('Transfer To Marked Title') %]"></eg-grid-action>
     <eg-grid-action handler="grid_actions.mark_damaged_wide" group="[% l('Item') %]"
       label="[% l('Mark Item Damaged') %]"></eg-grid-action>
+    <eg-grid-action handler="grid_actions.mark_discard_wide" group="[% l('Item') %]"
+      label="[% l('Mark Item Discard/Weed') %]"></eg-grid-action>
     <eg-grid-action handler="grid_actions.mark_missing_wide" group="[% l('Item') %]"
       label="[% l('Mark Item Missing') %]"></eg-grid-action>
     <eg-grid-action handler="grid_actions.retarget_wide" group="[% l('Hold') %]"
index c77ced8..96c402c 100644 (file)
 
         <p><b>[% l('Mark') %]</b></p>    
         <li><a href ng-click="selectedHoldingsDamaged()">[% l('Item as Damaged') %]</a></li>
+        <li><a href ng-click="selectedHoldingsDiscard()">[% l('Item as Discard/Weed') %]</a></li>
         <li><a href ng-click="selectedHoldingsMissing()">[% l('Item as Missing') %]</a></li>
     
         <p><b>[% l('Add') %]</b></p>    
index c0ac0c2..024271e 100644 (file)
@@ -47,6 +47,8 @@
 
   <eg-grid-action handler="selectedHoldingsDamaged" group="[% l('Mark') %]"
     label="[% l('Item as Damaged') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsDiscard" group="[% l('Mark') %]"
+    label="[% l('Item as Discard/Weed') %]"></eg-grid-action>
   <eg-grid-action handler="selectedHoldingsMissing" group="[% l('Mark') %]"
     label="[% l('Item as Missing') %]"></eg-grid-action>
     
index eff1448..85fd44e 100644 (file)
     label="[% l('Mark Items Damaged') %]">
   </eg-grid-action>
   <eg-grid-action
+    handler="showMarkDiscard"
+    label="[% l('Mark Items Discard/Weed') %]">
+  </eg-grid-action>
+  <eg-grid-action
     handler="show_mark_missing_pieces"
     label="[% l('Mark Missing Pieces') %]">
   </eg-grid-action>
index 4bd3762..2348f99 100644 (file)
@@ -45,6 +45,8 @@
     label="[% l('Transfer To Marked Title') %]"></eg-grid-action>
   <eg-grid-action handler="grid_actions.mark_damaged"
     label="[% l('Mark Item Damaged') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.mark_discard"
+    label="[% l('Mark Item Discard/Weed') %]"></eg-grid-action>
   <eg-grid-action handler="grid_actions.mark_missing"
     label="[% l('Mark Item Missing') %]"></eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
index 083edec..c385a6f 100644 (file)
@@ -48,6 +48,8 @@
     label="[% l('Transfer To Marked Title') %]"></eg-grid-action>
   <eg-grid-action handler="grid_actions.mark_damaged_wide"
     label="[% l('Mark Item Damaged') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.mark_discard_wide"
+    label="[% l('Mark Item Discard/Weed') %]"></eg-grid-action>
   <eg-grid-action handler="grid_actions.mark_missing_wide"
     label="[% l('Mark Item Missing') %]"></eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
index 14a6569..48700fc 100644 (file)
@@ -35,6 +35,8 @@
     label="[% l('Transfer To Marked Title') %]"></eg-grid-action>
   <eg-grid-action handler="grid_actions.mark_damaged"
     label="[% l('Mark Item Damaged') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.mark_discard"
+    label="[% l('Mark Item Discard/Weed') %]"></eg-grid-action>
   <eg-grid-action handler="grid_actions.mark_missing"
     label="[% l('Mark Item Missing') %]"></eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
index 886e93f..2095709 100644 (file)
     handler="showMarkDamaged"
     label="[% l('Mark Items Damaged') %]">
   </eg-grid-action>
+  <eg-grid-action
+    handler="showMarkDiscard"
+    label="[% l('Mark Items Discard/Weed') %]">
+  </eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
   <eg-grid-action 
     handler="abortTransit"
index 592365b..e5b1e1b 100644 (file)
@@ -27,6 +27,15 @@ s.LOCATION_ALERT_MSG =
     "{{copy.barcode()}}","{{copy.location().name()}}") %]';
 s.MARK_DAMAGED_CONFIRM = '[% l("Mark {{num_items}} items as DAMAGED?") %]';
 s.MARK_MISSING_CONFIRM = '[% l("Mark {{num_items}} items as MISSING?") %]';
+s.MARK_DISCARD_CONFIRM = '[% l("Mark {{num_items}} items as DICARD/WEED?") %]';
+s.MARK_ITEM_CHECKED_OUT = '[% l("Item {{barcode}} is checked out.") %]';
+s.MARK_ITEM_IN_TRANSIT = '[% l("Item {{barcode}} is in transit.") %]';
+s.MARK_ITEM_RESTRICT_DELETE = '[% l("Item {{barcode}} is in a status with a copy delete warning.") %]';
+s.MARK_ITEM_LAST_HOLD_COPY = '[% l("Item {{barcode}} is the last item to fill a hold.") %]';
+s.MARK_ITEM_CONTINUE = '[% l("Do you wish to continue marking it {{status}}?") %]';
+s.MARK_ITEM_CHECKIN_CONTINUE = '[% l("Do you wish to check it in and continue marking it {{status}}?") %]';
+s.MARK_ITEM_ABORT_CONTINUE = '[% l("Do you wish to abort the transit and continue marking it {{status}}?") %]';
+s.MARK_ITEM_FAILURE = '[% l("Marking of item {{barcode}} with status {{status}} failed: {{textcode}}") %]'
 s.ABORT_TRANSIT_CONFIRM = '[% l("Cancel {{num_transits}} transits?") %]';
 s.ROUTE_TO_HOLDS_SHELF = '[% l("Holds Shelf") %]';
 s.ROUTE_TO_CATALOGING = '[% l("Cataloging") %]';
index 2988d95..8bd06ad 100644 (file)
@@ -1671,8 +1671,22 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
+    $scope.selectedHoldingsDiscard = function () {
+        var copy_list = gatherSelectedRawCopies();
+        if (copy_list.length == 0) return;
+        egCirc.mark_discard(copy_list.map(function(cp) {
+            return {id: cp.id(), barcode: cp.barcode()};}).then(function() {
+            holdinsSvcInst.fetchAgain().then(function() {
+                $scop.holdingsGridDataProvider.refresh();
+            });
+        });
+    }
+
     $scope.selectedHoldingsMissing = function () {
-        egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function() {
+        var copy_list = gatherSelectedRawCopies();
+        if (copy_list.length == 0) return;
+        egCirc.mark_missing(copy_list.map(function(cp) {
+            return {id: cp.id(), barcode: cp.barcode()};}).then(function() {
             holdingsSvcInst.fetchAgain().then(function() {
                 $scope.holdingsGridDataProvider.refresh();
             });
index af59baf..4426ad0 100644 (file)
@@ -189,10 +189,17 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
         }]);
     }
 
+    $scope.selectedHoldingsDiscard = function () {
+        itemSvc.selectedHoldingsDiscard([{
+            id : $scope.args.copyId,
+            barcode : $scope.args.barcode
+        }]);
+    }
+
     $scope.selectedHoldingsMissing = function () {
         itemSvc.selectedHoldingsMissing([{
             id : $scope.args.copyId,
-            barcode : $scope.args.copyBarcode
+            barcode : $scope.args.barcode
         }]);
     }
 
@@ -527,6 +534,10 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
         itemSvc.selectedHoldingsDamaged(copyGrid.selectedItems());
     }
 
+    $scope.selectedHoldingsDiscard = function () {
+        itemSvc.selectedHoldingsDiscard(copyGrid.selectedItems());
+    }
+
     $scope.selectedHoldingsMissing = function () {
         itemSvc.selectedHoldingsMissing(copyGrid.selectedItems());
     }
index 965529b..d0e5106 100644 (file)
@@ -337,6 +337,20 @@ function($scope , $q , $window , $location , $timeout , egCore , checkinSvc , eg
 
     }
 
+    $scope.showMarkDiscard = function(items) {
+        var copies = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) {
+                copies.push(egCore.idl.toHash(item.acp));
+            }
+        });
+        if (copies.length) {
+            egCirc.mark_discard(copies).then(function() {
+                // update grid items?
+            });
+        }
+    }
+
     $scope.abortTransit = function(items) {
         var transit_ids = [];
         angular.forEach(items, function(item) {
index 2c6ba63..e4b7858 100644 (file)
@@ -182,6 +182,19 @@ function($scope , $window , $location , egCore , egGridDataProvider , egCirc) {
         }
     }
 
+    $scope.showMarkDiscard = function(items) {
+        var copyies = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) copies.push(egCore.idl.toHash(item.acp));
+        });
+
+        if (copies.length) {
+            egCirc.mark_discard(copies).then(function() {
+                // update grid items?
+            });
+        }
+    }
+
     $scope.showLastFewCircs = function(items) {
         if (items.length && (copy = items[0].acp)) {
             var url = $location.path(
index 6c2d30d..93a5d03 100644 (file)
@@ -1361,16 +1361,26 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                                 handle_checkin: !$scope.applyFine
                         }).then(function(resp) {
                             if (evt = egCore.evt.parse(resp)) {
-                                doRefresh = false;
-                                console.debug("mark damaged more information required. Pushing back.");
-                                service.mark_damaged({
-                                    id: params.id,
-                                    barcode: params.barcode,
-                                    charge: evt.payload.charge,
-                                    circ: evt.payload.circ,
-                                    refresh: params.refresh
-                                });
-                                console.error('mark damaged failed: ' + evt);
+                                egCore.pcrud.retrieve('ccs', 14)
+                                    .then(function(resp) {
+                                        service.handle_mark_item_event(
+                                            {id : params.id, barcode : params.barcode},
+                                            resp,
+                                            {
+                                                apply_fines: $scope.applyFine,
+                                                override_amount: $scope.billArgs.charge,
+                                                override_btype: $scope.billArgs.type,
+                                                override_note: $scope.billArgs.note,
+                                                handle_checkin: !$scope.applyFine
+                                            },
+                                            evt);
+                                    }).then(function(resp) {
+                                        // noop?
+                                        //if (doRefresh) egItem.add_barcode_to_list(params.barcode);
+                                    }, function(resp) {
+                                        doRefresh = false;
+                                        console.error('mark damaged failed: ' + evt);
+                                    });
                             }
                         }).then(function() {
                             if (doRefresh) egItem.add_barcode_to_list(params.barcode);
@@ -1381,30 +1391,146 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         }).result;
     }
 
-    service.mark_missing = function(copy_ids) {
+    service.handle_mark_item_event = function(copy, status, args, event) {
+        var dlogTitle, dlogMessage;
+        switch (event.textcode) {
+        case 'ITEM_TO_MARK_CHECKED_OUT':
+            dlogTitle = egCore.strings.MARK_ITEM_CHECKED_OUT;
+            dlogMessage = egCore.strings.MARK_ITEM_CHECKIN_CONTINUE;
+            args.handle_checkin = 1;
+            break;
+        case 'ITEM_TO_MARK_IN_TRANSIT':
+            dlogTitle = egCore.strings.MARK_ITEM_IN_TRANSIT;
+            dlogMessage = egCore.strings.MARK_ITEM_ABORT_CONTINUE;
+            args.handle_transit = 1;
+            break;
+        case 'ITEM_TO_MARK_LAST_HOLD_COPY':
+            dlogTitle = egCore.strings.MARK_ITEM_LAST_HOLD_COPY;
+            dlogMessage = egCore.strings.MARK_ITEM_CONTINUE;
+            args.handle_last_hold_copy = 1;
+            break;
+        case 'COPY_DELETE_WARNING':
+            dlogTitle = egCore.strings.MARK_ITEM_RESTRICT_DELETE;
+            dlogMessage = egCore.strings.MARK_ITEM_CONTINUE;
+            args.handle_copy_delete_warning = 1;
+            break;
+        case 'PERM_FAILURE':
+            console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
+                          event);
+            return service.exit_alert(egCore.strings.PERMISSION_DENIED,
+                                      {permission : event.ilsperm});
+            break;
+        default:
+            console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
+                          event);
+            return service.exit_alert(egCore.strings.MARK_ITEM_FAILURE,
+                                      {status : status.name(), barcode : copy.barcode,
+                                       textcode : event.textcode});
+            break;
+        }
         return egConfirmDialog.open(
-            egCore.strings.MARK_MISSING_CONFIRM, '',
-            {   num_items : copy_ids.length,
+            dlogTitle, dlogMessage,
+            {
+                barcode : copy.barcode,
+                status : status.name(),
+                ok : function () {},
+                cancel : function () {}
+            }
+        ).result.then(function() {
+            return service.mark_item(copy, status, args);
+        });
+    }
+
+    service.mark_item = function(copy, markstatus, args) {
+        if (!copy) return $q.when();
+
+        // If any new back end mark_item calls are added, also add
+        // them here to use them from the staff client.
+        // TODO: I didn't find any JS constants for copy status.
+        var req;
+        switch (markstatus.id()) {
+        case 2:
+            // Not implemented in the staff client, yet.
+            // req = "open-ils.circ.mark_item_bindery";
+            break;
+        case 4:
+            req = "open-ils.circ.mark_item_missing";
+            break;
+        case 9:
+            // Not implemented in the staff client, yet.
+            // req = "open-ils.circ.mark_item_on_order";
+            break;
+        case 10:
+            // Not implemented in the staff client, yet.
+            // req = "open-ils.circ.mark_item_ill";
+            break;
+        case 11:
+            // Not implemented in the staff client, yet.
+            // req = "open-ils.circ.mark_item_cataloging";
+            break;
+        case 12:
+            // Not implemented in the staff client, yet.
+            // req = "open-ils.circ.mark_item_reserves";
+            break;
+        case 13:
+            req = "open-ils.circ.mark_item_discard";
+            break;
+        case 14:
+            // Damaged is for handling of events. It's main handler is elsewhere.
+            req = "open-ils.circ.mark_item_damaged";
+            break;
+        }
+
+        return egCore.net.request(
+            'open-ils.circ',
+            req,
+            egCore.auth.token(),
+            copy.id,
+            args
+        ).then(function(resp) {
+            if (evt = egCore.evt.parse(resp)) {
+                return service.handle_mark_item_event(copy, markstatus, args, evt);
+            }
+        });
+    }
+
+    service.mark_discard = function(copies) {
+        return egConfirmDialog.open(
+            egCore.strings.MARK_DISCARD_CONFIRM, '',
+            {
+                num_items : copies.length,
                 ok : function() {},
                 cancel : function() {}
             }
         ).result.then(function() {
-            var promises = [];
-            angular.forEach(copy_ids, function(copy_id) {
-                promises.push(
-                    egCore.net.request(
-                        'open-ils.circ',
-                        'open-ils.circ.mark_item_missing',
-                        egCore.auth.token(), copy_id
-                    ).then(function(resp) {
-                        if (evt = egCore.evt.parse(resp)) {
-                            console.error('mark missing failed: ' + evt);
-                        }
-                    })
-                );
-            });
+            return egCore.pcrud.retrieve('ccs', 13)
+                .then(function(resp) {
+                    var promises = [];
+                    angular.forEach(copies, function(copy) {
+                        promises.push(service.mark_item(copy, resp, {}))
+                    });
+                    return $q.all(promises);
+                });
+        });
+    }
 
-            return $q.all(promises);
+    service.mark_missing = function(copies) {
+        return egConfirmDialog.open(
+            egCore.strings.MARK_MISSING_CONFIRM, '',
+            {
+                num_items : copies.length,
+                ok : function() {},
+                cancel : function() {}
+            }
+        ).result.then(function() {
+            return egCore.pcrud.retrieve('ccs', 4)
+                .then(function(resp) {
+                    var promises = [];
+                    angular.forEach(copies, function(copy) {
+                        promises.push(service.mark_item(copy, resp, {}))
+                    });
+                    return $q.all(promises);
+                });
         });
     }
 
index 393aeb3..dcc1d11 100644 (file)
@@ -744,20 +744,40 @@ function($window , $location , $timeout , egCore , egHolds , egCirc) {
         });
     }
 
+    service.mark_discard = function(items) {
+        var copies = items
+            .filter(function(item) { return Boolean(item.copy) })
+            .map(function(item) {
+                return {id: item.copy.id(), barcode: item.copy.barcode()}
+            });
+        if (copies.length)
+            egCirc.mark_discard(copies).then(service.refresh);
+    }
+
     service.mark_missing = function(items) {
-        var copy_ids = items
+        var copies = items
             .filter(function(item) { return Boolean(item.copy) })
-            .map(function(item) { return item.copy.id() });
-        if (copy_ids.length) 
-            egCirc.mark_missing(copy_ids).then(service.refresh);
+            .map(function(item) {
+                return {id: item.copy.id(), barcode: item.copy.barcode()}
+            });
+        if (copies.length)
+            egCirc.mark_missing(copies).then(service.refresh);
     }
 
     service.mark_missing_wide = function(items) {
-        var copy_ids = items
+        var copies = items
+            .filter(function(item) { return Boolean(item.hold.cp_id) })
+            .map(function(item) { return {id: item.hold.cp_id, barcode: item.hold.cp_barcode}; });
+        if (copies.length)
+            egCirc.mark_missing(copies).then(service.refresh);
+    }
+
+    service.mark_discard_wide = function(items) {
+        var copies = items
             .filter(function(item) { return Boolean(item.hold.cp_id) })
-            .map(function(item) { return item.hold.cp_id });
-        if (copy_ids.length) 
-            egCirc.mark_missing(copy_ids).then(service.refresh);
+            .map(function(item) { return {id: item.hold.cp_id, barcode: item.hold.cp_barcode}; });
+        if (copies.length)
+            egCirc.mark_discard(copies).then(service.refresh);
     }
 
     service.retarget = function(items) {
index c39f038..6382852 100644 (file)
@@ -644,8 +644,14 @@ function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog
         });
     }
 
+    service.selectedHoldingsDiscard = function (items) {
+        egCirc.mark_discard(items.map(function(el){return {id : el.id, barcode : el.barcode};})).then(function(){
+            angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
+        });
+    }
+
     service.selectedHoldingsMissing = function (items) {
-        egCirc.mark_missing(items.map(function(el){return el.id;})).then(function(){
+        egCirc.mark_missing(items.map(function(el){return {id : el.id, barcode : el.barcode};})).then(function(){
             angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
         });
     }
diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/enhanced-mark-item-functionality.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/enhanced-mark-item-functionality.adoc
new file mode 100644 (file)
index 0000000..9e02269
--- /dev/null
@@ -0,0 +1,112 @@
+Enhanced Mark Item Functionality
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Evergreen's Mark Item Damaged and Mark Item Missing functionality has
+been enhanced, and the ability to mark an item with the Discard/Weed
+status has been added.  This enhancement affects both the Evergreen
+back end code and the staff client.
+
+Staff Client Changes
+++++++++++++++++++++
+
+The option to "Mark Item as Discard/Weed" has been added to areas
+where the option(s) to "Mark Item as Missing" and/or "Mark Item as
+Damaged" appear.  This is primarily in the action menus on the
+following interfaces:
+
+ * Item Status
+ * Checkin
+ * Renew
+ * Holds Pull List
+ * Patron Holds List
+ * Record Holds List
+ * Holds Shelf
+ * Holdings Edit
+
+This new option allows staff to mark a copy with the Discard/Weed
+status quickly and easily without necessarily requiring the
+intervention of cataloging staff.  In order to mark an item with the
+Discard/Weed status, staff will require either the `MARK_ITEM_DISCARD`
+or `UPDATE_COPY status` at the item's owning library.  (NOTE: This
+permission choice is consistent with the permission requirements for
+the current Mark Item Damaged or Missing functionality.)
+
+If the item to be marked Discard/Weed is checked out to a patron, the
+staff will be presented with a dialog informing them that the item is
+checked out and asking if they would like to check it in and proceed.
+If they choose to continue, the item will be checked in and then
+marked with the Discard/Weed status.  If the staff person chooses to
+cancel, then the item will not be checked in, and it will not be
+marked Discard/Weed.  The Mark Item Missing functionality has also
+been changed to exhibit this behavior with checked out items.  The
+Mark Item Damaged functionality already handles checked out item.
+
+Should the item have a status of In Transit at the time it is to be
+marked, then staff will be prompted to abort the transit before
+proceeding with changing the item's status.  If they choose to abort
+the transit and they have the permission to do so, the transit will be
+aborted and the item's status changed.  If they choose to cancel, then
+the transit will not be aborted and the item's status will remain
+unchanged.  This change applies to all three of the current Mark Item
+statuses: Missing, Damaged, and Discard/Weed.
+
+Marking an item Discard/Weed is typically one step away from deleting
+the item.  For this reason, if the item to be marked Discard/Weed is
+not in a Checked Out or In Transit status, but it is in a status that
+restricts item deletion, the staff will be presented with a dialog
+notifying them of the item's status and asking if they wish to
+proceed.  If staff choose to proceed and they have the
+`COPY_DELETE_WARNING.override` permission, then the item will be
+marked with the Discard/Weed status.  Naturally, the item's status
+will be unchanged if they choose not to proceed.  This change does not
+affect the marking of an item as Missing or Damaged.
+
+Marking an item as Discard/Weed has one more additional check that the
+other statuses do not.  If the item being marked as Discard/Weed is
+the last copy that can fill a hold, then staff will also be notified
+of this condition and asked if they wish to continue.  In this case,
+there is no permission required.  Whether or not the item is marked as
+Discard/Weed in this case depends solely on the staff's choice.
+
+Back End Changes
+++++++++++++++++
+
+In order to accommodate the presentation of dialogs and overrides in
+the staff client, the `OpenILS::Application::Circ` module's method for
+marking item statuses has had a few changes made.  Firstly, the code
+of the `mark_item` function has been rearranged to a more logical
+flow.  Most of the condition and permission checks are made before
+creating a transaction.  Secondly, it has been modified to return 3
+new events when certain conditions are met:
+
+ * `ITEM_TO_MARK_CHECKED_OUT`
+ * `ITEM_TO_MARK_IN_TRANSIT`
+ * `ITEM_TO_MARK_LAST_HOLD_COPY`
+
+The `COPY_DELETE_WARNING` event will be returned when attempting to
+mark an item with the Discard/Weed status and the status has the
+`restrict_copy_delete` flag set to true.
+
+The function now also recognizes a hash of extra arguments for all
+statuses and not just for the mark Damaged functionality.  This
+argument hash can be used to bypass or override any or all of the
+above mentioned events.  Each event has a corresponding argument that
+if set to a "true" value will cause the `mark_item` to bypass the
+given event.  These argument flags are, respectively:
+
+ * `handle_checkin`
+ * `handle_transit`
+ * `handle_last_hold_copy`
+ * `handle_copy_delete_warning`
+
+The code to mark an item damaged still accepts its previous hash
+arguments in addition to these new ones.
+
+The function still returns other errors and events as before.  It
+still returns 1 on success.
+
+It is also worth noting here that the staff client can be easily
+extended with the ability to mark items into the other statuses
+offered by the back end functions.  Most of the staff client
+functionality is implemented in two functions with placeholders in the
+main function (`egCirc.mark_item`) for the unimplemented statuses.