From c111f843939add8426867848f54623c1e403322e Mon Sep 17 00:00:00 2001 From: Kyle Huckins Date: Wed, 25 Oct 2017 23:43:08 +0000 Subject: [PATCH] CAT-152 Update Items Interface 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Signed-off-by: Kyle Huckins 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 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 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 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 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 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 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 Changes to be committed: modified: Open-ILS/web/js/ui/default/staff/acq/update_items/app.js --- .../lib/OpenILS/Application/Acq/Lineitem.pm | 9 +- .../src/templates/staff/acq/update_items/index.tt2 | 37 + .../staff/acq/update_items/t_update_items.tt2 | 256 +++++++ .../src/templates/staff/cat/catalog/t_catalog.tt2 | 5 + .../js/ui/default/staff/acq/update_items/app.js | 802 +++++++++++++++++++++ .../web/js/ui/default/staff/cat/catalog/app.js | 6 + 6 files changed, 1113 insertions(+), 2 deletions(-) create mode 100644 Open-ILS/src/templates/staff/acq/update_items/index.tt2 create mode 100644 Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2 create mode 100644 Open-ILS/web/js/ui/default/staff/acq/update_items/app.js diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm index 19ce965e90..3744b2002b 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm @@ -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 index 0000000000..33e48e4262 --- /dev/null +++ b/Open-ILS/src/templates/staff/acq/update_items/index.tt2 @@ -0,0 +1,37 @@ +[% + WRAPPER "staff/base.tt2"; + ctx.page_title = l("Update Items"); + ctx.page_app = "egUpdateItems"; +%] + +[% BLOCK APP_JS %] + + + + + + + + + +[% END %] + +
+ +[% 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 index 0000000000..6e9bd3286c --- /dev/null +++ b/Open-ILS/src/templates/staff/acq/update_items/t_update_items.tt2 @@ -0,0 +1,256 @@ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + +
+
+
+
+
+
+ +
+ +
+
+
+
+
[% l('Additional Note:') %]
+ +
+
+
+ +
+ +
+ + +
+
+
+
+
[% l('Call Number') %]
+ + + + +
+
+
+ +
+
+ +
+ +
[% l('Owning Library') %]
+
[% l('Volumes') %]
+
+
+
[% l('Call Number') %]
+
[% l('Copies') %]
+
+
+
[% l('Barcode') %]
+
[% l('Status') %]
+
+
+
+
+
+
+
+
+ {{strings.warningNoSelectedPO}}
+
+ {{strings.warningNoAvailablePO}}
+
+
+
+
+
+ {{strings.warningUnknownError}} +
+
+
+
+
+ {{org.shortname}} +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
[% l('Template') %]
+ +
+
+
+ + +
+
+
+
+ [% l('Circulation Modifer') %] +
+
+
+
+ +
+
+
+
+
+
+ [% l('Circulate?') %] +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ [% l('Location/Collection') %] +
+
+
+
+ +
+
+
+
+
+
+ [% l('Price') %] +
+
+
+
+ +
+
+
+
+
+ + +
+
[% l('Line Item Notes') %]
+
+
+
+
+
{{strings.warningNoNotes}}
+
+
+ +
+
+
\ No newline at end of file diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2 index c9b733d733..83e02385c4 100644 --- a/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2 +++ b/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2 @@ -91,6 +91,11 @@ [% l('View/Place Orders') %] +
  • + + [% l('Update Items') %] + +
  • 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 index 0000000000..d221d64b13 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/acq/update_items/app.js @@ -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: + '
    ' + + '
    Lineitem
    ' + + '' + + '
    ' + + '', + controller : "UpdateCtrl" + } +}) + +.directive("egProductOrderNotes", function() { + return { + restrict: 'E', + replace: true, + template: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '{{note.create_time | date: "yyyy-MM-dd"}}' + + '
    ' + + '
    ' + + '{{note.value}}' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + controller: "UpdateCtrl" + } +}) + +.directive("egLineItemSaveButton", function() { + return { + restrict: 'E', + template: + '', + 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 diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js index 7d525c5f43..9319a9f461 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js @@ -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; -- 2.11.0