CAT-152 Update Items Interface
authorKyle Huckins <khuckins@catalyte.io>
Wed, 25 Oct 2017 23:43:08 +0000 (23:43 +0000)
committerBill Erickson <berickxx@gmail.com>
Thu, 21 Mar 2019 19:46:23 +0000 (15:46 -0400)
Rebase and squash of Catalyte AngularJS Update Items port.  See commits
below.  Original code:

kcls/dev/catalyst-khuckins/CAT-151-Update-Items-Webby-Port

Basic frontend for update items interface.

Access via one of the following paths:
1. Direct:
      [Hostname]/eg/staff/acq/update_items/[Record ID]
2. Through Catalog:
      On a record, Other Actions->Update Items

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
new file:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
new file:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
new file:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-167 Update Items App.js Map

Comment through and add TODOs in app.js to map out what we
can utilize and what we should strip out.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-153 Retrieve Record for Item Update Interface

Set record ID to dataKey from $routeParams.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-154 Lineitems Dropdown for Update Items

- Move record_id into service object.
- Add new directive egProductOrderDropdown to handle the dropdown.
- Add new function in itemSvc to fetch lineitems based on bib record.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-155 Display Lineitem Details

- Add service function to convert lineitem to object
- Add service function to fetch needed copy information
- Slight refactor to CAT-154 code to accomodate objectification
- Create egProductOrderVolumes directive to handle display of Org and Volume
information
- Create egProductOrderCopy directive to handle display of Copy information

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-163 Display Line Item Notes

- Add notes to lineitem object
- Display Lineitem notes for selected PO  under Line Item Notes section
of update items interface.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-158 Update Items Save Functionality

- Items that have had their Call Number or Barcode edited
update upon pressing the "Save Changes" or "Save & Exit"
buttons.
- "Save Changes" will reload the page upon saving.
- "Save & Exit" closes the page upon saving.
- If a volume's call number has changed, a call will be made
to find_or_create_volume, creating a new volume only if an
applicable one doesn't already exist.
- Addition to retrieve_lineitem API: Optional flag
flesh_li_details_copy to retrieve the acp object tied to a
lineitem.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-161 Update Items Add Notes Functionality

- Allow adding notes to lineitem directly from the Update Items
Interface.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-168 service.saveChanges Refactor

- Refactor service.saveChanges to handle saving changes to items
in different Orgs.

 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-159 Print Labels Functionality

- Ticking the print labels checkbox will open the Print
Item Labels interface for every item in the currently
selected lineitem upon saving changes. Any changes made
in the update items interface will be represented in the
Print Item Labels interface.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-160 Print Worksheet Functionality

- Ticking the Print Worksheet box will open the Worksheet for the selected
lineitem, ready for the user to print.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-166 Edit Items Section

- Edit Items Section now covers the saving of Circ Modifier,
Circulate?, Location/Collection, and Price. Upon saving changes,
those fields will be accounted for when updating each copy.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-166 Edit Items Template Selector

- Template Selector now applies changes to the circ modifier,
circulate?, location/collection, and price fields based on the
values in the selected template.
- Template Selection will be cleared upon changing Lineitem
selection.
- Whitespace cleanup for previous commit

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-165 Edit Item Attributes

- Edit Item Attributes button now leads to the copy editor for
each copy in the currently selected lineitem in a new tab.
- A prompt will appear on the Update Items interface after
activating the Edit Item Attributes button warning that without
a refresh, there could be inconsistancy between changes made in the
Copy Editor and changes made in the Edit Items portion of the Update
Items interface.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
        modified:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-156 Call Number Batch Apply

- The Apply button adjacent to the Call Number Batch Apply
field will now Apply the contents of that field to each volume
Call Number field.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-167 App.js cleanup & Polishing

- Remove unnecessary code from App.js
- Set controller for several Update Items directives to UpdateCtrl
- Disable input fields and buttons when no line item selected
- Add notices when no line items available, no lineitem selected, and
no notes to display
- Reduce amount of network calls made when fetching lineitems

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-169 Update Items Hotkeys

- Angular-Hotkeys implementation for required hotkeys.
- Minor refactors to better accomodate hotkey code.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js
modified:   Open-ILS/web/js/ui/default/staff/cat/catalog/app.js

CAT-152 Template Readability Adjustments

- Break overly long lines into multiple lines.
- Remove unnecessary strings defined in index

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2

CAT-167 Only display on-order and received lis

- Remove extraneous console.log
- Ensure only lineitems that are on-order or received are
displayed

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-156 Autogenerate Barcodes & Checkdigit

- Implimentation of autogenerate barcodes with checkdidgets generation working
- Apply CSS changes and open ngToasts when Use Checkdigit
is checked and an invalid barcode is found.
- Consolidate egProductOrderCopies and egProductOrderVolumes
into t_update_items.tt2.

Signed-off-by: Alex Cautley <acautley@catalyte.io>
Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
modified:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-167 Checkdigit Patch

- Allow Barcodes that are only numbers to be recognized as
valid by the checkdigit validation function.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-167 Color-blind Friendliness for Barcode Field

- Apply additional stylings and add glyphicon to Barcode field
when Use Checkdigit is enabled based on valid or invalid barcode.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-167 Initial Progress Dialog

- Add an instance of egProgressDialog while fetching lineitems

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-167 Save & Print Optimizations

- Move print option service logic into separate function.
- Adapt egItems print_spine_labels as separate function within egUpdateItems.
- Open Print Label and Worksheet windows after save, but before refresh, to better
handle large lineitems(1000+).
- Allow both Worksheet and Spine Label print windows to open without signifigant lag
time between each other.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

CAT-167 Handle Fetch error with large lineitems

- Apply error message when lineitem is unable to display due
to fetch error in edge cases where there is a lineitem with 1k+
entries occasionally stopping org information from being fetched
for other lineitems.
- Small rearrangement of code in UpdateCtrl to make things more
readable.
- Addition of Glyphicon warning sign when displaying error messages.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/templates/staff/acq/update_items/index.tt2
modified:   Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

lp1744762 Lineitems by Bib filter multiple states

- Allow lineitem_state to take an array of strings, rather than
just a single string.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm

CAT-176 Refactor Update Items Fetch

- Utilize lineitem_state to properly fetch lineitems with the fixed code
from lp1744762

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/web/js/ui/default/staff/acq/update_items/app.js

Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm
Open-ILS/src/templates/staff/acq/update_items/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
Open-ILS/web/js/ui/default/staff/acq/update_items/app.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/cat/catalog/app.js

index 19ce965..3744b20 100644 (file)
@@ -152,7 +152,8 @@ sub retrieve_lineitem_impl {
         flesh_fields => {
             jub => ['purchase_order', 'picklist'], # needed for permission check
             acqlid => [],
-            acqlin => []
+            acqlin => [],
+            acp => []
         }
     };
 
@@ -169,6 +170,9 @@ sub retrieve_lineitem_impl {
         push(@{$fields->{acqlid}}, 'fund'         ) if $$options{flesh_fund};
         push(@{$fields->{acqlid}}, 'fund_debit'   ) if $$options{flesh_fund_debit};
         push(@{$fields->{acqlid}}, 'cancel_reason') if $$options{flesh_cancel_reason};
+        push(@{$fields->{acqlid}}, 'eg_copy_id') if $$options{flesh_li_details_copy};
+        push(@{$fields->{acp}}, 'status') if $$options{flesh_li_details_copy};
+        push(@{$fields->{acp}}, 'call_number') if $$options{flesh_li_details_copy};
     }
 
     if($$options{clear_marc}) { # avoid fetching marc blob
@@ -387,6 +391,7 @@ sub lineitem_search {
 }
 
 __PACKAGE__->register_method (
+    # TODO: Authoritative-ify
     method    => 'lineitems_related_by_bib',
     api_name  => 'open-ils.acq.lineitems_for_bib.by_bib_id',
     stream    => 1,
@@ -453,7 +458,7 @@ sub lineitems_related_by_bib {
     }
 
     if ($options && defined $options->{lineitem_state}) {
-        $query->{'where'}{'jub'}{'state'} = $options->{lineitem_state};
+        $query->{'where'}{'+jub'}{'state'} = $options->{lineitem_state};
     }
 
     if ($options && defined $options->{po_state}) {
diff --git a/Open-ILS/src/templates/staff/acq/update_items/index.tt2 b/Open-ILS/src/templates/staff/acq/update_items/index.tt2
new file mode 100644 (file)
index 0000000..33e48e4
--- /dev/null
@@ -0,0 +1,37 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Update Items"); 
+  ctx.page_app = "egUpdateItems";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/file.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/acq/update_items/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/circ.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/item.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.UPDATE_ITEMS_HOTKEY_SAVE = "[% l('Save Changes') %]";
+    s.UPDATE_ITEMS_HOTKEY_SAVE_EXIT = "[% l('Save & Exit') %]";
+    s.UPDATE_ITEMS_NO_CHANGES = "[% l('There are no changes to save.') %]";
+    s.UPDATE_ITEMS_WARNING_NO_NOTES = "[% l('No notes to display.') %]";
+    s.UPDATE_ITEMS_WARNING_NO_SELECTED_PO = "[% l('Select a Lineitem to display data.') %]";
+    s.UPDATE_ITEMS_WARNING_NO_AVAILABLE_PO = "[% l('No Lineitems to display.') %]";
+    s.UPDATE_ITEMS_WARNING_INVALID_CHECKDIGIT = "[% l('is not a valid barcode.') %]";
+    s.UPDATE_ITEMS_WARNING_FAILED_TO_DISPLAY_LINEITEM = "[% l('We were unable to display this lineitem due to an unknown error. Refresh to try again.') %]";
+    s.UPDATE_ITEMS_REFRESH_REQUEST_TITLE = "[% l('Data may have changed') %]";
+    s.UPDATE_ITEMS_REFRESH_REQUEST = "[% l('Data for the copies in the selected Lineitem may have been modified via the Edit Item Attributes button. Continuing without refreshing may override these changes.') %]";
+    s.UPDATE_ITEMS_REFRESH = "[% l('Refresh Page') %]";
+    s.UPDATE_ITEMS_NOREFRESH = "[% l('Continue Without Refreshing') %]";
+    s.UPDATE_ITEMS_NONE = "[% l('<NONE>') %]";
+}]);
+</script>
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2 b/Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2
new file mode 100644 (file)
index 0000000..6e9bd32
--- /dev/null
@@ -0,0 +1,256 @@
+<eg-record-summary record-id="record_id" record="summaryRecord"></eg-record-summary>
+<div class="container-fluid pad-vert" eg-update-item-hotkey>
+
+<div class="row pad-vert">
+    <div class="col-xs-12 col-md-2">
+        <button class="btn btn-default" type="button"  ng-disabled="!currentLineItem" ng-click="autogenBarcode()"
+          eg-accesskey="alt+shift+g" eg-accesskey-desc="[% l('Autogenerate Barcodes') %]">
+            [% l('Autogenerate Barcodes') %]
+        </button>
+    </div>
+    <div class="col-xs-12 col-sm-6 col-md-2 form-check form-check-inline">
+      <label class="form-check-label">
+        <input class="form-check-input" type="checkbox" ng-model="itemArgs.use_checkdigit"
+          eg-accesskey="alt+t" eg-accesskey-desc="[% l('Use Checkdigit') %]">[% l('Use Checkdigit') %]
+      </label>
+    </div>
+    <div class="col-xs-12 col-sm-6 col-md-2 form-check form-check-inline">
+      <label class="form-check-label">
+        <input class="form-check-input" type="checkbox" ng-model="printOptions.print_label"
+          eg-accesskey="alt+shift+p" eg-accesskey-desc="[% l('Print Labels') %]">[% l('Print Labels') %]
+      </label>
+    </div>
+    <div class="col-xs-12 col-sm-6 col-md-2 form-check form-check-inline">
+      <label class="form-check-label">
+        <input class="form-check-input" type="checkbox" ng-model="printOptions.print_worksheet">[% l('Print Worksheet') %]
+      </label>
+    </div>
+    <div class="col-xs-4">
+        <eg-line-item-save-button content="strings.saveChanges" note-data="noteData"
+          print-options="printOptions" item-args="itemArgs" selected="currentLineItem">
+        </eg-line-item-save-button>
+        <eg-line-item-save-button exit="true" content="strings.saveExit"
+          note-data="noteData" access-key="strings.saveAccessKey"
+          print-options="printOptions" item-args="itemArgs" selected="currentLineItem">
+        </eg-line-item-save-button>
+        <button class="btn btn-default" type="button" ng-click="editItemAttributes()"
+          ng-disabled="!currentLineItem" eg-accesskey='alt+shift+e'
+          eg-accesskey-desc="[% l('Edit Item Attributes') %]">
+          [% l('Edit Item Attributes') %]
+        </button>
+    </div>
+</div>
+<div class="row">
+    <div class="col-xs-12 col-sm-6 col-md-4">
+        <div class="input-group">
+            <div class="input-group-addon">
+              <label class="form-check-label">
+                <input class="form-check-input" type="checkbox" ng-model="noteData.add_notes">[% l('Add Notes:') %]
+              </label>
+            </div>
+            <input class="form-control" type="text" ng-model="noteData.note_a">
+        </div>
+    </div>
+    <div class="col-xs-12 col-sm-6 col-md-3">
+        <div class="input-group">
+            <div class="input-group-addon">[% l('Additional Note:') %]</div>
+            <input class="form-control" type="text" ng-model="noteData.note_b">
+        </div>
+    </div>
+</div>
+
+<hr />
+
+<div class="row">
+
+    <!--Update Items Section -->
+    <div class="col-xs-12 col-md-7">
+        <div class="row bg-info">
+            <div class="col-xs-12 col-md-5">
+                <div class="input-group">
+                    <div class="input-group-addon">[% l('Call Number') %]</div>
+                    <input class="form-control center-block" ng-model="batchApply.callnumber"
+                    type="text" />
+                    <span class="input-group-btn">
+                      <button class="btn btn-default" type="button" eg-accesskey="alt+shift+a"
+                        ng-click="callnumberBatchApply()" ng-disabled="!currentLineItem"
+                        eg-accesskey-desc="[% l('Batch Apply Callnumber') %]">
+                        [% l('Apply') %]
+                      </button>
+                    </span>
+                </div>
+            </div>
+            <div class="col-xs-12 col-md-5">
+                <eg-line-item-dropdown></eg-line-item-dropdown>
+            </div>
+        </div>
+
+        <div class="row pad-vert">
+            <!--ng-repeat for each lineitem -->
+            <div class="col-xs-6 col-sm-3 col-md-2"><b>[% l('Owning Library') %]</b></div>
+            <div class="col-xs-6 col-sm-3 col-md-1"><b>[% l('Volumes') %]</b></div>
+            <div class="col-xs-12 col-sm-6 col-md-9">
+                <div class="row"> <!-- ng-repeat volumes -->
+                    <div class="col-xs-6 col-md-3"><b>[% l('Call Number') %]</b></div>
+                    <div class="col-xs-6 col-md-2" ><b>[% l('Copies') %]</b></div>
+                    <div class="col-xs-12 col-md-7">
+                        <div class="row"> <!-- ng-repeat copies -->
+                            <div class="col-xs-6"><b>[% l('Barcode') %]</b></div>
+                            <div class="col-xs-6"><b>[% l('Status') %]</b></div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="row pad-vert" ng-if="!selectedPO">
+            <div class="col-xs-12">
+                <div class="alert alert-warning" ng-if="purchaseOrders.length">
+                {{strings.warningNoSelectedPO}}</div>
+                <div class="alert alert-danger" ng-if="!purchaseOrders.length">
+                <i class="glyphicon glyphicon-warning-sign"></i> {{strings.warningNoAvailablePO}}</div>
+            </div>
+        </div>
+        <div class="row pad-vert" ng-if="lineitemDisplayErrorFlag">
+            <div class="col-xs-12">
+                <div class="alert alert-danger">
+                <i class="glyphicon glyphicon-warning-sign"></i> {{strings.warningUnknownError}}
+                </div>
+            </div>
+        </div>
+        <div class="row pad-vert" ng-if="currentLineItem"
+          ng-repeat="org in currentLineItem.orgs">
+          <div class="col-xs-6 col-sm-3 col-md-2">
+            <span class="center-block">{{org.shortname}}</span>
+          </div>
+          <div class="col-xs-6 col-sm-3 col-md-1">
+            <input class="form-control" ng-disabled="true" value="{{org.vols.length}}">
+          </div>
+          <div class="col-xs-12 col-sm-6 col-md-9">
+            <div class="row" style="padding-bottom:20px" ng-repeat="volume in org.vols">
+              <div class="col-xs-6 col-md-3">
+                <input class="form-control" ng-model="volume.cn_label" ng-blur="updateVolCopy()">
+              </div>
+              <div class="col-xs-6 col-md-2">
+                <input class="form-control" ng-disabled="true"
+                  ng-value="volume.copies.length">
+              </div>
+              <div class="col-xs-12 col-md-7">
+                <div class="row" style="padding-bottom:20px" ng-repeat="copy in volume.copies">
+                  <div class="col-xs-6">
+                    <div class="input-group">
+                        <input class="form-control" ng-model="copy.barcode" ng-blur="barcodeCheck(copy)"
+                          ng-class="barcodeBoxValidation(copy,itemArgs,true)"
+                          ng-style="!itemArgs.use_checkdigit ? {
+                            'border-top-right-radius':'4px','border-bottom-right-radius':'4px',
+                            'border-top-left-radius':'4px','border-bottom-left-radius':'4px'} : {
+                            'border-top-right-radius':'0px','border-bottom-right-radius':'0px',
+                            'border-top-left-radius':'4px','border-bottom-left-radius':'4px'} ">
+                        <div class="input-group-addon" ng-class="{'alert-danger': copy._invalidBarcode}" ng-if="itemArgs.use_checkdigit">
+                            <span class="glyphicon" ng-class="{'glyphicon-warning-sign': copy._invalidBarcode, 'glyphicon-ok': !copy._invalidBarcode}"></span>
+                        </div>
+                    </div>
+                  </div>
+                  <div class="col-xs-6">
+                    <input class="form-control" ng-value="copy.status.name" ng-disabled="true">
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+    </div>
+
+    <!-- Micro-Item Attribute Editor & Notes Section -->
+    <div class="col-xs-12 col-md-5">
+        <form novalidate class="css-form" name="forms.myForm">
+        <div class="row bg-info">
+            <div class="col xs-12 col-md-6">
+                <div class="input-group">
+                    <div class="input-group-addon">[% l('Template') %]</div>
+                    <select class="form-control" ng-model="selectedTemplate" ng-change="applyTemplate(selectedTemplate)" 
+                        ng-options="template for template in template_name_list" ng-disabled="!currentLineItem">
+                        <option value="">{{strings.noneOption}}</option>
+                    </select>
+                </div>
+            </div>
+        </div>
+
+        <!-- Circ Modifier & Circulate? -->
+        <div class="row pad-vert">
+            <div class="col-xs-6">
+                <div class="row bg-info">
+                    <div class="col-xs-12">
+                        <b>[% l('Circulation Modifer') %]</b>
+                    </div>
+                </div>
+                <div class="row pad-vert">
+                    <div class="nullable col-xs-12">
+                        <select class="form-control" ng-model="itemArgs.circ_modifier" ng-disabled="!currentLineItem"
+                            ng-options="m.code() as m.name() for m in circ_modifier_list | orderBy: 'name()'">
+                            <option value="">{{strings.noneOption}}</option>
+                        </select>
+                    </div>
+                </div>
+            </div>
+            <div class="col-xs-6">
+                <div class="row bg-info">
+                    <div class="col-xs-12">
+                        <b>[% l('Circulate?') %]</b>
+                    </div>
+                </div>
+                <div class="row pad-vert">
+                    <div class="col-xs-12">
+                        <div class="btn-group" data-toggle="buttons">
+                            <label ng-class="circulateButtonClasses('t')" ng-model="itemArgs.circulate"
+                              uib-btn-radio="true" uib-uncheckable="!currentLineItem">[% l('Yes') %]</label>
+                            <label ng-model="itemArgs.circulate" ng-class="circulateButtonClasses('f')"
+                              uib-btn-radio="false" uib-uncheckable="!currentLineItem">[% l('No') %]</label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+             <!-- Circ Library & Price -->
+            <div class="col-xs-6">
+                <div class="row bg-info">
+                    <div class="col-md-12">
+                        <b>[% l('Location/Collection') %]</b>
+                    </div>
+                </div>
+                <div class="row pad-vert">
+                    <div class="col-md-12">
+                        <select class="form-control" ng-model="itemArgs.location" ng-disabled="!currentLineItem"
+                        ng-options="l.id() as i18n.ou_qualified_location_name(l) for l in location_list"></select>
+                    </div>
+                </div>
+            </div>
+            <div class="col-xs-6">
+                <div class="row bg-info">
+                    <div class="col-md-12">
+                        <b>[% l('Price') %]</b>
+                    </div>
+                </div>
+                <div class="row pad-vert">
+                    <div class="col-md-12">
+                        <input class="form-control" ng-model="itemArgs.price" ng-disabled="!currentLineItem">
+                    </div>
+                </div>
+            </div>
+        </div>
+        </form>
+
+        <!-- Line Item Notes Section -->
+        <div class="row bg-info">
+            <div class="col-xs-12"><h5 class="center-block">[% l('Line Item Notes') %]</h5></div>
+        </div>
+        <div class="row pad-vert"></div>
+        <div class="row" ng-if="!currentLineItem || !currentLineItem.notes.length">
+            <div class="col-xs-12">
+                <div class="alert alert-warning">{{strings.warningNoNotes}}</div>
+            </div>
+        </div>
+        <eg-product-order-notes></eg-product-order-notes>
+    </div>
+</div>
+</div>
\ No newline at end of file
index c9b733d..83e0238 100644 (file)
                         [% l('View/Place Orders') %]
                    </a>
             </li>
+             <li role="menuitem">
+                    <a ng-click="view_update_items()" href="">
+                        [% l('Update Items') %]
+                    </a>
+            </li>
         </ul>
         </div>
     </div>
diff --git a/Open-ILS/web/js/ui/default/staff/acq/update_items/app.js b/Open-ILS/web/js/ui/default/staff/acq/update_items/app.js
new file mode 100644 (file)
index 0000000..d221d64
--- /dev/null
@@ -0,0 +1,802 @@
+/**
+ * Update Items
+ */
+
+angular.module('egUpdateItems',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.filter('boolText', function(){
+    return function (v) {
+        return v == 't';
+    }
+})
+
+.config(['ngToastProvider', function(ngToastProvider) {
+  ngToastProvider.configure({
+    verticalPosition: 'bottom',
+    animation: 'fade'
+  });
+}])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+    var resolver = {
+        delay : ['egStartup','egProgressDialog', function(egStartup,egProgressDialog) { return egStartup.go().then(egProgressDialog.open()); }]
+    };
+
+    $routeProvider.when('/acq/update_items/:dataKey', {
+        templateUrl: './acq/update_items/t_update_items',
+        controller: 'UpdateCtrl',
+        resolve : resolver
+    });
+})
+
+.factory('updateItemSvc',
+       ['egCore','$q','$routeParams','$window','$timeout','egItem','hotkeys','egProgressDialog',
+function(egCore , $q , $routeParams , $window , $timeout , egItem , hotkeys , egProgressDialog) {
+
+    var service = {
+        record_id : $routeParams.dataKey,
+        currently_generating : false,
+        auto_gen_barcode : false,
+        barcode_checkdigit : false,
+        lineitems : [],
+        selected_lineitem: {}
+    };
+
+    service.fetchLineItems = function() {
+        return egCore.net.request(
+            'open-ils.acq', 'open-ils.acq.lineitems_for_bib.by_bib_id',
+            egCore.auth.token(), service.record_id, {
+                flesh_po: true,
+                flesh_li_details: true,
+                flesh_notes: true,
+                flesh_li_details_copy: true,
+                lineitem_state: ['on-order', 'received']
+            }
+        ).then(function() {
+            egProgressDialog.close();
+        },null,function(jub) {
+            var duplicateLineItem = false;
+            var purchaseOrder = service.objectifyLineItems(jub);
+            angular.forEach(service.lineitems, function(li) {
+                if (li.li_id == purchaseOrder.li_id) duplicateLineItem = true;
+            });
+            if (!duplicateLineItem) service.lineitems.push(purchaseOrder);
+        });
+    }
+
+    service.find_or_create_volume = function(cn_label, record_id, ou_id) {
+        return egCore.net.request(
+            'open-ils.cat', 
+            'open-ils.cat.call_number.find_or_create',
+            egCore.auth.token(), 
+            cn_label, 
+            record_id, 
+            ou_id
+        ).then(function(res) {
+            if (!res.existed) console.debug("service.find_or_create_volume: Creating new volume");
+            return res.acn_id;
+        });
+    }
+
+    service.updateCopies = function(acpArray, exit, print_options, copy_ids) {
+        egCore.net.request(
+            'open-ils.cat',
+            'open-ils.cat.asset.copy.fleshed.batch.update',
+            egCore.auth.token(),
+            acpArray
+        ).then(function(res) {
+            if (res != 1) console.debug("service.updateCopies: Copies failed to update");
+
+            if (print_options) {
+                $timeout(function() {
+                    service.handlePrintOptions(copy_ids, print_options);
+                }).then(function() {service.handlePostSave(exit);});
+            } else {
+            service.handlePostSave(exit);
+            }
+        });
+    }
+
+    // Copied over from egItems for Optimizing saving
+    service.print_spine_labels = function(copy_ids){
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'print-labels-these-copies', {
+                copies : copy_ids
+            }
+        ).then(function(key) {
+            if (!key) alert('service.print_spine_labels: Could not create anonymous cache key!');
+            return key;
+        });
+    }
+
+    service.updateLineitemNotes = function(acqlin_list) {
+        egCore.net.request(
+            'open-ils.acq',
+            'open-ils.acq.lineitem_note.cud.batch',
+            egCore.auth.token(),
+            acqlin_list
+        ).then(function(res) {
+            if (res != 1) console.debug("service.updateLineitemNotes: Notes failed to update");
+        });
+    }
+
+    service.orgArrayCleanup = function(orgs) {
+        var finalOrgs = [];
+
+        angular.forEach(orgs, function(o) {
+            var hasVol = false;
+            if (o.vols.length > 0) finalOrgs.push(o);
+        });
+        return finalOrgs;
+    }
+
+    service.objectifyOrgs = function(jub) {
+        var orgs = [];
+        var volumes = [];
+
+        // Populate our Org units for the parent array
+        angular.forEach(egCore.org.list(), function(org) {
+            orgs.push({id: org.id(), shortname: org.shortname(), vols: []});
+        });
+
+        //Fill out our volumes list
+        angular.forEach(jub.lineitem_details(), function(acqlid) {
+            volume = egCore.idl.toHash(acqlid.eg_copy_id());
+            var existingVolume = false;
+            var owningLib = egCore.org.get(acqlid.owning_lib()).shortname();
+            angular.forEach(orgs, function(o) {
+
+                //Volume already exists? Push copy to existing volume
+                angular.forEach(o.vols, function(v) {
+                    if (volume.call_number.label == v.cn_label && o.shortname == owningLib) {
+                        existingVolume = true;
+                        v.copies.push(service.objectifyCopy(volume));
+                    }
+
+                });
+
+                // If the volume doesn't exist yet, push a new volume
+                if (!existingVolume) {
+                    if (acqlid.eg_copy_id().call_number) {
+                        var tempVolume = service.objectifyVolume(acqlid);
+                        if (tempVolume.owning_lib == o.shortname) o.vols.push(tempVolume);
+                    }
+                }
+            });
+        });
+        orgs = service.orgArrayCleanup(orgs);
+        return orgs;
+    }
+
+    //Fill out our Volume object
+    service.objectifyVolume = function(acqlid) {
+        acp = egCore.idl.toHash(acqlid.eg_copy_id());
+        return {
+            id: acp.call_number.id,
+            owning_lib: egCore.org.get(acqlid.owning_lib()).shortname(),
+            cn_label: acp.call_number.label,
+            copies: [acp]
+        }
+    }
+
+    //Fill out our Copy object
+    service.objectifyCopy = function(acp) {
+        return egCore.idl.toHash(acp);
+    }
+
+    service.objectifyNotes = function(acqlinArray) {
+        var notes = [];
+
+        angular.forEach(acqlinArray, function(note) {
+            notes.push({
+                id: note.id(),
+                creator: note.creator(),
+                create_time: note.create_time(),
+                value: note.value()
+            });
+        });
+
+        return notes;
+    }
+
+    service.objectifyLineItems = function(jub) {
+        /* We want to make the lineitem into an object with the following information:
+        {
+            dropdownLabel: PO: POID / LI: LIID
+            po_id:
+            li_id:
+            rawData:
+            notes: [{id,create_time,value,creator}],
+            orgs: [{
+                id:
+                shortname:
+                vols: [{
+                    id:
+                    owning_lib:
+                    cn_label:
+                    copies: [acp]
+                }] 
+            }]
+        } */
+        var purchaseOrder = {};
+        purchaseOrder.orgs = service.objectifyOrgs(jub);
+        //Defining this here so we can use ng-options
+        purchaseOrder.dropdownLabel = "PO: " + jub.purchase_order().id() + " / LI: " + jub.id();
+        purchaseOrder.po_id = jub.purchase_order().id();
+        purchaseOrder.li_id = jub.id();
+        purchaseOrder.notes = service.objectifyNotes(jub.lineitem_notes());
+        purchaseOrder.rawData = jub;
+
+        return purchaseOrder;
+    }
+
+    service.generateNote = function(note, li_id) {
+        var acqlin = new egCore.idl.acqlin();
+        acqlin.isnew(true);
+        acqlin.lineitem(li_id);
+        acqlin.value(note);
+
+        return acqlin;
+    }
+
+    service.saveChanges = function(args) {
+        var liToSave = service.getCurrentLineItem();
+        var changesToSave = false;
+        copy_ids = [];
+
+        if (!liToSave) {
+            console.debug("service.saveChanges: No Lineitem Selected.");
+            return;
+        }
+
+        angular.forEach(liToSave.orgs, function(org) {
+            angular.forEach(org.vols, function(volume) {
+                angular.forEach(volume.copies, function(copy) {
+                    copy_ids.push(copy.id);
+                });
+            });
+        });
+
+        if (args.add_notes) {
+            var notes = [];
+
+            if (!args.note_a) {
+                console.debug("service.saveChanges: No data in note fields.");
+                return;
+            }
+            var acqlin_a = service.generateNote(args.note_a, liToSave.li_id);
+            notes.push(acqlin_a);
+            if (args.note_b) {
+                var acqlin_b = service.generateNote(args.note_b, liToSave.li_id);
+                notes.push(acqlin_b);
+            }
+
+            changesToSave = true;
+            service.updateLineitemNotes(notes);
+        }
+        if (args.copies.length) {
+            changesToSave = true;
+            service.updateCopies(args.copies, args.exit, args.print_options, copy_ids);
+        } else if (changesToSave) {
+            if (args.print_options) {
+                service.handlePrintOptions(copy_ids, args.print_options);
+            }
+            service.handlePostSave(args.exit);
+        } else {
+            if (args.print_options) {
+                service.handlePrintOptions(copy_ids, args.print_options);
+            }
+            console.debug("service.saveChanges: There are no changes to save.")
+        }
+    }
+
+    service.handlePrintOptions = function(copy_ids, print_options) {
+        if (print_options.print_label && print_options.print_worksheet) {
+            service.print_spine_labels(copy_ids).then(function(key) {
+                var lurl = egCore.env.basePath + 'cat/printlabels/' + key;
+                var wurl ='/eg/acq/lineitem/worksheet/' + service.getCurrentLineItem().li_id;
+                $timeout(function() { $window.open(lurl, '_blank') });
+                $timeout(function() { $window.open(wurl, '_blank') });
+            });
+        } else if (print_options.print_label && !print_options.print_worksheet) {
+            service.print_spine_labels(copy_ids).then(function(key) {
+                var url = egCore.env.basePath + 'cat/printlabels/' + key;
+                $timeout(function() { $window.open(url, '_blank') });
+            });
+        } else if (print_options.print_worksheet && !print_options.print_label) {
+            var url = '/eg/acq/lineitem/worksheet/' + service.getCurrentLineItem().li_id;
+            $timeout(function() { $window.open(url, '_blank') });
+        }
+    }
+
+    service.handlePostSave = function(exit) {
+        if (exit) $window.close();
+        $window.location.reload();
+    }
+
+    // Search service.lineitems for the copy with a specific ID
+    service.findCopy = function(cp_id) {
+        var unHashedCopy;
+        angular.forEach(service.lineitems, function(lineitem) {
+           angular.forEach(lineitem.orgs, function(org) {
+               angular.forEach(org.vols, function(volume) {
+                   angular.forEach(volume.copies, function(copy) {
+                        if (copy.id == cp_id) {
+                            unHashedCopy = egCore.idl.fromHash('acp',copy);
+                            unHashedCopy.status(copy.status.id);
+                            unHashedCopy.call_number(copy.call_number.id);
+                        }
+                   });
+               });
+           });
+        });
+        return unHashedCopy;
+    }
+
+    // Compare two copies, returning true if the copies differ on specified fields
+    service.compareCopy = function(copy_a, copy_b) {
+        if (copy_a.barcode() != copy_b.barcode()) return true;
+        if (copy_a.call_number() != copy_b.call_number()) return true;
+        if (copy_a.location() != copy_b.location()) return true;
+        if (copy_a.circ_modifier() != copy_b.circ_modifier()) return true;
+        if (copy_a.circulate() != copy_b.circulate()) return true;
+        if (copy_a.price() != copy_b.price()) return true;
+        return false;
+    }
+
+    service.updateLocalLineItemData = function(li) {
+        if (li) {
+            service.selected_lineitem = li;
+        } else {
+            console.debug("service.updateLocalLineItemData: No Lineitem specified");
+        }
+    }
+
+    service.getLineItems = function() {
+        if(!service.lineitems.length) {
+            console.debug("service.getLineItems: No Lineitems registered. Fetching...");
+            service.fetchLineItems();
+        }
+        return service.lineitems;
+    }
+
+    service.getCurrentLineItem = function() {
+        return service.selected_lineitem;
+    }
+
+    service.nextBarcode = function(bc,bcCount,use_checkdigit) {
+        service.currently_generating = true;
+        return egCore.net.request(
+            'open-ils.cat',
+            'open-ils.cat.item.barcode.autogen',
+            egCore.auth.token(),
+            bc, bcCount, { checkdigit: use_checkdigit }
+        ).then(function(resp) { // get_barcodes
+            var evt = egCore.evt.parse(resp);
+            if (!evt) return resp;
+            return '';
+        });
+    };
+
+    service.checkBarcode = function(bc) {
+        if (bc != Number(bc)) return false;
+        bc = bc.toString();
+        // "16.00" == Number("16.00"), but the . is bad.
+        // Throw out any barcode that isn't just digits
+        if (bc.search(/\D/) != -1) return false;
+        var last_digit = bc.substr(bc.length-1);
+        var stripped_barcode = bc.substr(0,bc.length-1);
+        return service.barcodeCheckdigit(stripped_barcode).toString() == last_digit;
+    };
+
+    service.barcodeCheckdigit = function(bc) {
+        var reverse_barcode = bc.toString().split('').reverse();
+        var check_sum = 0; var multiplier = 2;
+        for (var i = 0; i < reverse_barcode.length; i++) {
+            var digit = reverse_barcode[i];
+            var product = digit * multiplier; product = product.toString();
+            var temp_sum = 0;
+            for (var j = 0; j < product.length; j++) {
+                temp_sum += Number( product[j] );
+            }
+            check_sum += Number( temp_sum );
+            multiplier = ( multiplier == 2 ? 1 : 2 );
+        }
+        check_sum = check_sum.toString();
+        var next_multiple_of_10 = (check_sum.match(/(\d*)\d$/)[1] * 10) + 10;
+        var check_digit = next_multiple_of_10 - Number(check_sum); if (check_digit == 10) check_digit = 0;
+        return check_digit;
+    };
+
+    service.get_locations = function(orgs) {
+        return egCore.pcrud.search('acpl',
+            {owning_lib : orgs, deleted : 'f'},
+            {
+                flesh : 1,
+                flesh_fields : {
+                    acpl : ['owning_lib']
+                },
+                order_by : { acpl : 'name' }
+            },
+            {atomic : true}
+        );
+    };
+
+    service.get_circ_mods = function() {
+        if (egCore.env.ccm)
+            return $q.when(egCore.env.ccm.list);
+
+        return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'ccm');
+                return list;
+            }
+        );
+
+    };
+
+    service.addHotkey = function(key, desc, elm) {
+        angular.forEach(key.split(' '), function(k) {
+            hotkeys.add({
+                combo: k,
+                description: desc,
+                callback: function(e) {
+                    e.preventDefault();
+                    return $timeout(function(){$(elm).trigger('click')});
+                }
+            });
+        });
+    }
+
+    return service;
+}])
+
+.directive("egUpdateItemHotkey", function() {
+    return {
+        restrict: 'A',
+        controller: ['$scope','$q','$timeout','$element','updateItemSvc','egCore',
+            function ( $scope , $q , $timeout , $element , updateItemSvc , egCore) {
+
+            function find_accesskeys(elm) {
+                    elm = angular.element(elm);
+                    if (elm.attr('eg-accesskey')) {
+                        updateItemSvc.addHotkey(
+                            elm.attr('eg-accesskey'),
+                            elm.attr('eg-accesskey-desc'),
+                            elm
+                        );
+                    }
+                    angular.forEach(elm.children(), find_accesskeys);
+                }
+
+                egCore.startup.go().then(
+                    function() {
+                        $timeout(function(){find_accesskeys($element)});
+                    }
+                );
+        }]
+    }
+})
+
+.directive("egLineItemDropdown", function() {
+    return {
+        restrict: 'E',
+        replace: true,
+        template:
+        '<div class="input-group">' +
+            '<div class="input-group-addon">Lineitem</div>' +
+              '<select class="form-control" ng-model="selectedPO"' +
+                'ng-change="updatePO()" ng-disabled="!purchaseOrders.length"' +
+                'ng-options="li.dropdownLabel for li in purchaseOrders track by li.po_id">' +
+                '<option value="">{{strings.noneOption}}</option>' +
+              '</select>' +
+            '</div>' +
+        '</div>',
+        controller : "UpdateCtrl"
+    }
+})
+
+.directive("egProductOrderNotes", function() {
+    return {
+        restrict: 'E',
+        replace: true,
+        template:
+            '<div class="row" ng-repeat="note in currentLineItem.notes track by note.id">' +
+              '<div class="col-md-12 well">' +
+                '<div class="row">' +
+                  '<div class="col-md-2">' +
+                    '{{note.create_time | date: "yyyy-MM-dd"}}' +
+                  '</div>' +
+                  '<div class="col-md-10">' +
+                    '{{note.value}}' +
+                  '</div>' +
+                '</div>' +
+              '</div>' +
+            '</div>',
+        controller: "UpdateCtrl"
+    }
+})
+
+.directive("egLineItemSaveButton", function() {
+    return {
+        restrict: 'E',
+        template:
+          '<button class="btn btn-default" type="button"' +
+            'ng-click="saveLineItem(exit)" ng-disabled="!selected" eg-accesskey="{{accessKey}}" eg-accesskey-desc="{{content}}" >' +
+              '{{content}}' +
+            '</button>',
+        scope: {exit: "=", content: "=", noteData: "=", printOptions: "=", itemArgs: "=", selected: "=", accessKey: "="},
+        controller : ['$scope','$q','$timeout','$element','$window','egConfirmDialog','egAlertDialog','egProgressDialog','updateItemSvc','egCore',
+            function ( $scope , $q , $timeout , $element , $window , egConfirmDialog , egAlertDialog , egProgressDialog , updateItemSvc , egCore) {
+                $scope.saveLineItem = function(exit) {
+                    if ($scope.selected) {
+                        copies = $scope.collectCopies();
+
+                        egProgressDialog.open();
+                        $timeout(function() {
+                            updateItemSvc.saveChanges({
+                                exit: exit,
+                                copies: copies,
+                                add_notes: $scope.noteData.add_notes,
+                                note_a: $scope.noteData.note_a,
+                                note_b: $scope.noteData.note_b,
+                                print_options: $scope.printOptions
+                            });
+                        },1000);
+                    } else {
+                        return egAlertDialog.open(
+                            egCore.strings.UPDATE_ITEMS_NO_CHANGES,
+                        ).result;
+                    }
+                }
+
+                $scope.collectCopies = function() {
+                    var copies = [];
+                    angular.forEach(updateItemSvc.getCurrentLineItem().orgs, function(org) {
+                        angular.forEach(org.vols, function(volume) {
+                            var promises = [];
+                            promises.push(updateItemSvc.find_or_create_volume(volume.cn_label, updateItemSvc.record_id, org.id).then(function(res) {
+                                return res;
+                            }));
+
+                            $q.all(promises).then(function(vol_id) {
+                                angular.forEach(volume.copies, function(copy) {
+                                    var orig_cp = updateItemSvc.findCopy(copy.id);
+                                    var cp = egCore.idl.fromHash('acp', copy);
+                                    cp.status(copy.status.id);
+                                    cp.call_number(vol_id[0]);
+                                    if ($scope.itemArgs.location) cp.location($scope.itemArgs.location);
+                                    if ($scope.itemArgs.circ_modifier) cp.circ_modifier($scope.itemArgs.circ_modifier);
+                                    if ($scope.itemArgs.circulate == true) cp.circulate('t');
+                                    if ($scope.itemArgs.circulate == false) cp.circulate('f');
+                                    if ($scope.itemArgs.price) cp.price($scope.itemArgs.price);
+
+                                    if (updateItemSvc.compareCopy(cp, orig_cp)) {
+                                        cp.ischanged(true);
+                                        copies.push(cp);
+                                    }
+                                });
+                            });
+                        });
+                    });
+                    return copies;
+                }
+            }],
+            link: function(scope, element, attrs) {
+                var noteData = scope.noteData;
+                var itemArgs = scope.itemArgs;
+
+                scope.$watch('noteData', function(value) {
+                    noteData = value;
+                });
+                scope.$watch('itemArgs', function(value) {
+                    itemArgs = value;
+                });
+            }
+    }
+})
+
+.controller('UpdateCtrl',
+       ['$scope','$q','$window','$routeParams','$location','$timeout','$filter','egCore','updateItemSvc','egConfirmDialog','ngToast',
+function($scope , $q , $window , $routeParams , $location , $timeout , $filter , egCore , updateItemSvc , egConfirmDialog , ngToast) {
+    var staff_initials = egCore.auth.user().second_given_name();
+    var noteDate = $filter('date')(new Date(), "dd/MM/yy");
+    $scope.record_id = $routeParams.dataKey;
+    $scope.purchaseOrders = updateItemSvc.getLineItems();
+    $scope.i18n = egCore.i18n;
+    $scope.strings = {
+        noneOption : egCore.strings.UPDATE_ITEMS_NONE,
+        warningNoSelectedPO : egCore.strings.UPDATE_ITEMS_WARNING_NO_SELECTED_PO,
+        warningNoAvailablePO : egCore.strings.UPDATE_ITEMS_WARNING_NO_AVAILABLE_PO,
+        warningNoNotes : egCore.strings.UPDATE_ITEMS_WARNING_NO_NOTES,
+        warningUnknownError : egCore.strings.UPDATE_ITEMS_WARNING_FAILED_TO_DISPLAY_LINEITEM,
+        saveChanges : egCore.strings.UPDATE_ITEMS_HOTKEY_SAVE,
+        saveExit : egCore.strings.UPDATE_ITEMS_HOTKEY_SAVE_EXIT,
+        saveAccessKey : "alt+shift+s",
+    }
+    $scope.itemArgs = {use_checkdigit: false};
+    $scope.templates = {};
+    $scope.template_name_list = [];
+    $scope.noteData = {
+        add_notes: false,
+        note_a: "PROC:" + staff_initials + " " + noteDate,
+        note_b: null
+    }
+    $scope.circ_modifier_list = [];
+    $scope.location_list = [];
+
+    updateItemSvc.get_circ_mods().then(function(list) {
+        $scope.circ_modifier_list = list;
+    });
+
+    updateItemSvc.get_locations(egCore.auth.user().ws_ou()).then(function(list) {
+        $scope.location_list = list;
+    });
+
+    $scope.circulateButtonClasses = function(circulateLabel) {
+        if (circulateLabel == $scope.itemArgs.circulate) {
+            return "btn btn-primary ng-untouched ng-valid ng-dirty active ng-not-empty ng-valid-parse";
+        } else {
+            return "btn btn-primary"
+        }
+    }
+
+    $scope.updateVolCopy = function() {
+        updateItemSvc.updateLocalLineItemData($scope.currentLineItem);
+    }
+
+    $scope.fetchTemplates = function () {
+        egCore.hatch.getItem('cat.copy.templates').then(function(templates) {
+            if (templates) {
+                $scope.templates = templates;
+                $scope.template_name_list = Object.keys(templates);
+            }
+        });
+    }
+    $scope.fetchTemplates();
+
+    $scope.applyTemplate = function(template) {
+        angular.forEach($scope.templates[template], function (value,key) {
+            if (!angular.isObject(value)) {
+                $scope.itemArgs[key] = angular.copy(value);
+            }
+        });
+        egCore.hatch.setItem('cat.copy.last_template', template);
+    }
+
+    $scope.updatePO = function() {
+        $scope.selectedTemplate = '';
+        $scope.lineitemDisplayErrorFlag = false;
+
+        if ($scope.selectedPO && $scope.selectedPO.orgs.length) {
+            $scope.currentLineItem = $scope.selectedPO;
+            updateItemSvc.updateLocalLineItemData($scope.selectedPO);
+            $scope.itemArgs.location = $scope.selectedPO.orgs[0].vols[0].copies[0].location;
+            $scope.itemArgs.circ_modifier = $scope.selectedPO.orgs[0].vols[0].copies[0].circ_modifier;
+            $scope.itemArgs.circulate = $scope.selectedPO.orgs[0].vols[0].copies[0].circulate;
+            $scope.itemArgs.price = $scope.selectedPO.orgs[0].vols[0].copies[0].price;
+        } else {
+            if ($scope.selectedPO) $scope.lineitemDisplayErrorFlag = true;
+            $scope.currentLineItem = null;
+            $scope.itemArgs.location = null;
+            $scope.itemArgs.circ_modifier = null;
+            $scope.itemArgs.circulate = null;
+            $scope.itemArgs.price = null;
+        }
+    }
+
+    $scope.callnumberBatchApply = function() {
+        angular.forEach(updateItemSvc.getCurrentLineItem().orgs, function(org) {
+           angular.forEach(org.vols, function(vol) {
+              if ($scope.batchApply) {
+                vol.cn_label = $scope.batchApply.callnumber;
+              } else {
+                  vol.cn_label = "";
+              }
+           });
+        });
+    }
+
+    $scope.autogenBarcode = function() {   
+        var volumeCount = 0
+
+        angular.forEach(updateItemSvc.getCurrentLineItem().orgs, function(org) {
+           angular.forEach(org.vols, function(vol) {
+                volumeCount = volumeCount + vol.copies.length;
+           });
+        });
+
+        updateItemSvc.nextBarcode(
+            $scope.selectedPO.orgs[0].vols[0].copies[0].barcode,
+            volumeCount - 1,
+            $scope.itemArgs.use_checkdigit).then(function(res){
+                $scope.barcodes = res;
+                var currentCopy = 0;
+                angular.forEach(updateItemSvc.getCurrentLineItem().orgs, function(org) {
+                    angular.forEach(org.vols, function(vol) {
+                        for (c = 0; c < vol.copies.length; c++) { 
+                            if (currentCopy != 0) {
+                            vol.copies[c].barcode = $scope.barcodes[currentCopy - 1];}
+                            currentCopy++
+                        }
+                    });     
+                })  
+         });
+    }
+
+    $scope.barcodeBoxValidation = function(copy,args,cssBoolean) {
+        if (!args.use_checkdigit) {
+            copy._invalidBarcode = false;
+            return true;
+        } else {
+            var barcodeNum;
+            var barcode = copy.barcode;
+            var barcodeSplit = barcode.split(/^\D*/);
+            if(barcodeSplit[1]) {
+                barcodeNum = barcodeSplit[1];
+            } else {
+                barcodeNum = barcodeSplit[0];
+            }
+            var isValid = updateItemSvc.checkBarcode(barcodeNum);
+
+            if (cssBoolean && !isValid) {
+                copy._invalidBarcode = true;
+                return 'alert-danger';
+            } else if (cssBoolean && isValid) {
+                copy._invalidBarcode = false;
+            }
+            return isValid;
+        }
+    }
+
+    $scope.barcodeCheck = function(copy) {
+        $scope.updateVolCopy();
+        var validBarcode = $scope.barcodeBoxValidation(copy,$scope.itemArgs);
+        if (!validBarcode) {
+            ngToast.danger(copy.barcode + " " + egCore.strings.UPDATE_ITEMS_WARNING_INVALID_CHECKDIGIT);
+        }
+    }
+
+    $scope.editItemAttributes = function() {
+        var copyIds = [];
+        angular.forEach(updateItemSvc.getCurrentLineItem().orgs, function(org) {
+            angular.forEach(org.vols, function(vol) {
+                angular.forEach(vol.copies, function(copy) {
+                    copyIds.push(copy.id);
+                });
+            });
+        });
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'edit-these-copies', {
+                record_id: $scope.record_id,
+                copies: copyIds,
+                hide_vols : true,
+                hide_copies : false
+            }
+        ).then(function(key) {
+            if (key) {
+                var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                $timeout(function() { $window.open(url, '_blank') });
+                return egConfirmDialog.open(
+                  egCore.strings.UPDATE_ITEMS_REFRESH_REQUEST_TITLE,
+                  egCore.strings.UPDATE_ITEMS_REFRESH_REQUEST,
+                  null,
+                  egCore.strings.UPDATE_ITEMS_REFRESH,
+                  egCore.strings.UPDATE_ITEMS_NOREFRESH
+                ).result.then(function() {
+                    $window.location.reload();
+                });
+            } else {
+                alert('Could not create anonymous cache key!');
+            }
+        });
+    }
+}])
\ No newline at end of file
index 7d525c5..9319a9f 100644 (file)
@@ -1022,6 +1022,12 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         $timeout(function() { $window.open(url, '_blank') });
     }
 
+    $scope.view_update_items = function() {
+        if (!$scope.record_id) return;
+        var url = egCore.env.basePath + 'acq/update_items/' + $scope.record_id;
+        $timeout(function() { $window.open(url, '_blank') });
+    }
+
     $scope.replaceBarcodes = function() {
         var copy_list = gatherSelectedRawCopies();
         if (copy_list.length == 0) return;