From 258b293dc30e48a49fcd3519ccc991427d64c1a8 Mon Sep 17 00:00:00 2001 From: senator Date: Thu, 19 Aug 2010 18:18:21 +0000 Subject: [PATCH] Serials: an alternative batch receiving interface, to support certain heavy-barcoding workflows. Still needs some things hooked up in the middle layer to create serial.units. Still needs some configurability. Access from "Actions for this Record" in the staff-client-wrapped OPAC for a record with subscriptions and items attached. git-svn-id: svn://svn.open-ils.org/ILS/trunk@17269 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- Open-ILS/examples/fm_IDL.xml | 14 +- .../OpenILS/Application/Circ/CopyLocations.pm | 13 +- .../perlmods/OpenILS/Application/Serial.pm | 269 ++++++- Open-ILS/web/opac/locale/en-US/lang.dtd | 49 ++ .../staff_client/chrome/content/cat/opac.js | 46 ++ .../staff_client/chrome/content/cat/opac.xul | 1 + .../chrome/content/main/constants.js | 3 +- .../chrome/locale/en-US/offline.properties | 1 + .../server/locale/en-US/serial.properties | 10 + .../server/serial/batch_receive.js | 711 ++++++++++++++++++ .../server/serial/batch_receive.xul | 32 + .../server/serial/batch_receive_overlay.xul | 159 ++++ .../xul/staff_client/server/skin/serial.css | 11 + 13 files changed, 1310 insertions(+), 9 deletions(-) create mode 100644 Open-ILS/xul/staff_client/server/serial/batch_receive.js create mode 100644 Open-ILS/xul/staff_client/server/serial/batch_receive.xul create mode 100644 Open-ILS/xul/staff_client/server/serial/batch_receive_overlay.xul create mode 100644 Open-ILS/xul/staff_client/server/skin/serial.css diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index df5c59c8e3..eb79fa0a6c 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -1781,6 +1781,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -1800,6 +1801,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -3169,10 +3171,10 @@ SELECT usr, - + - + @@ -3190,6 +3192,14 @@ SELECT usr, + + + + + + + + diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Circ/CopyLocations.pm b/Open-ILS/src/perlmods/OpenILS/Application/Circ/CopyLocations.pm index 0ebe1c7bc5..3930641b95 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Circ/CopyLocations.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Circ/CopyLocations.pm @@ -18,22 +18,29 @@ __PACKAGE__->register_method( signature => q/ Retrieves the ranged set of copy locations for the requested org. If no org is provided, all copy locations are returned - @param authtoken The login session key @param orgId The org location id + @param noi18n No i18n in result + @param flesh_owning_lib Flesh owning lib in results @return An array of copy location objects /); sub cl_retrieve_all { - my( $self, $client, $org_id, $no_i18n ) = @_; + my ($self, $client, $org_id, $no_i18n, $flesh_owning_lib) = @_; if(!$org_id) { my $otree = $U->get_org_tree(); $org_id = $otree->id; } + my $second_cstore_arg = {"no_i18n" => scalar($no_i18n)}; + if ($flesh_owning_lib) { + $second_cstore_arg->{"flesh"} = 1; + $second_cstore_arg->{"flesh_fields"} = {"acpl" => ["owning_lib"]}; + } + return new_editor()->search_asset_copy_location([{ owning_lib => $U->get_org_full_path($org_id) - }, {"no_i18n" => scalar($no_i18n)}]); + }, $second_cstore_arg]); } __PACKAGE__->register_method( diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Serial.pm b/Open-ILS/src/perlmods/OpenILS/Application/Serial.pm index 9bb0a18889..a61bc52926 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Serial.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Serial.pm @@ -40,10 +40,12 @@ use warnings; use OpenILS::Application; use base qw/OpenILS::Application/; use OpenILS::Application::AppUtils; +use OpenILS::Event; use OpenSRF::AppSession; -use OpenSRF::Utils qw/:datetime/;; -use OpenSRF::Utils::Logger qw($logger); +use OpenSRF::Utils qw/:datetime/; +use OpenSRF::Utils::Logger qw/:logger/; use OpenILS::Utils::CStoreEditor q/:funcs/; +use OpenILS::Utils::Fieldmapper; use OpenILS::Utils::MFHD; use MARC::File::XML (BinaryEncoding => 'utf8'); my $U = 'OpenILS::Application::AppUtils'; @@ -58,7 +60,6 @@ my %MFHD_TAGS_BY_NAME = ( $MFHD_NAMES[0] => '853', $MFHD_NAMES[1] => '854', $MFHD_NAMES[2] => '855'); - # helper method for conforming dates to ISO8601 sub _cleanse_dates { my $item = shift; @@ -70,6 +71,14 @@ sub _cleanse_dates { return 0; } +sub _get_mvr { + $U->simplereq( + "open-ils.search", + "open-ils.search.biblio.record.mods_slim.retrieve", + @_ + ); +} + ########################################################################## # item methods @@ -1604,4 +1613,258 @@ sub serial_caption_and_pattern_retrieve_batch { ); } +__PACKAGE__->register_method( + "method" => "bre_by_identifier", + "api_name" => "open-ils.serial.biblio.record_entry.by_identifier", + "stream" => 1, + "signature" => { + "desc" => "Find instances of biblio.record_entry given a search token" . + " that could be a value for any identifier defined in " . + "config.metabib_field", + "params" => [ + {"desc" => "Search token", "type" => "string"}, + {"desc" => "Options: require_subscriptions, add_mvr, is_actual_id" . + " (all boolean)", "type" => "object"} + ], + "return" => { + "desc" => "Any matching BREs, or if the add_mvr option is true, " . + "objects with a 'bre' key/value pair, and an 'mvr' " . + "key-value pair. BREs have subscriptions fleshed on.", + "type" => "object" + } + } +); + +sub bre_by_identifier { + my ($self, $client, $term, $options) = @_; + + return new OpenILS::Event("BAD_PARAMS") unless $term; + + $options ||= {}; + my $e = new_editor(); + + my @ids; + + if ($options->{"is_actual_id"}) { + @ids = ($term); + } else { + my $cmf = + $e->search_config_metabib_field({"field_class" => "identifier"}) + or return $e->die_event; + + my @identifiers = map { $_->name } @$cmf; + my $query = join(" || ", map { "id|$_: $term" } @identifiers); + + my $search = create OpenSRF::AppSession("open-ils.search"); + my $search_result = $search->request( + "open-ils.search.biblio.multiclass.query.staff", {}, $query + )->gather(1); + $search->disconnect; + + # Un-nest results. They tend to look like [[1],[2],[3]] for some reason. + @ids = map { @{$_} } @{$search_result->{"ids"}}; + + unless (@ids) { + $e->disconnect; + return undef; + } + } + + my $bre = $e->search_biblio_record_entry([ + {"id" => \@ids}, { + "flesh" => 2, "flesh_fields" => { + "bre" => ["subscriptions"], + "ssub" => ["owning_lib"] + } + } + ]) or return $e->die_event; + + if (@$bre && $options->{"require_subscriptions"}) { + $bre = [ grep { @{$_->subscriptions} } @$bre ]; + } + + $e->disconnect; + + if (@$bre) { # re-evaluate after possible grep + if ($options->{"add_mvr"}) { + $client->respond( + {"bre" => $_, "mvr" => _get_mvr($_->id)} + ) foreach (@$bre); + } else { + $client->respond($_) foreach (@$bre); + } + } + + undef; +} + +__PACKAGE__->register_method( + "method" => "get_receivable_items", + "api_name" => "open-ils.serial.items.receivable.by_subscription", + "stream" => 1, + "signature" => { + "desc" => "Return all receivable items under a given subscription", + "params" => [ + {"desc" => "Authtoken", "type" => "string"}, + {"desc" => "Subscription ID", "type" => "number"}, + ], + "return" => { + "desc" => "All receivable items under a given subscription", + "type" => "object" + } + } +); + +__PACKAGE__->register_method( + "method" => "get_receivable_items", + "api_name" => "open-ils.serial.items.receivable.by_issuance", + "stream" => 1, + "signature" => { + "desc" => "Return all receivable items under a given issuance", + "params" => [ + {"desc" => "Authtoken", "type" => "string"}, + {"desc" => "Issuance ID", "type" => "number"}, + ], + "return" => { + "desc" => "All receivable items under a given issuance", + "type" => "object" + } + } +); + +sub get_receivable_items { + my ($self, $client, $auth, $term) = @_; + + my $e = new_editor("authtoken" => $auth); + return $e->die_event unless $e->checkauth; + + # XXX permissions + + my $by = ($self->api_name =~ /by_(\w+)$/)[0]; + + my %where = ( + "issuance" => {"issuance" => $term}, + "subscription" => {"+siss" => {"subscription" => $term}} + ); + + my $item_ids = $e->json_query( + { + "select" => {"sitem" => ["id"]}, + "from" => {"sitem" => "siss"}, + "where" => { + %{$where{$by}}, "date_received" => undef + }, + "order_by" => {"sitem" => ["id"]} + } + ) or return $e->die_event; + + return undef unless @$item_ids; + + foreach (map { $_->{"id"} } @$item_ids) { + $client->respond( + $e->retrieve_serial_item([ + $_, { + "flesh" => 3, + "flesh_fields" => { + "sitem" => ["stream", "issuance"], + "sstr" => ["distribution"], + "sdist" => ["holding_lib"] + } + } + ]) + ); + } + + $e->disconnect; + undef; +} + +__PACKAGE__->register_method( + "method" => "get_receivable_issuances", + "api_name" => "open-ils.serial.issuances.receivable", + "stream" => 1, + "signature" => { + "desc" => "Return all issuances with receivable items given " . + "a subscription ID", + "params" => [ + {"desc" => "Authtoken", "type" => "string"}, + {"desc" => "Subscription ID", "type" => "number"}, + ], + "return" => { + "desc" => "All issuances with receivable items " . + "(but not the items themselves)", "type" => "object" + } + } +); + +sub get_receivable_issuances { + my ($self, $client, $auth, $sub_id) = @_; + + my $e = new_editor("authtoken" => $auth); + return $e->die_event unless $e->checkauth; + + # XXX permissions + + my $issuance_ids = $e->json_query({ + "select" => { + "siss" => [ + {"transform" => "distinct", "column" => "id"} + ] + }, + "from" => {"siss" => "sitem"}, + "where" => { + "subscription" => $sub_id, + "+sitem" => {"date_received" => undef} + } + }) or return $e->die_event; + + $client->respond($e->retrieve_serial_issuance($_->{"id"})) + foreach (@$issuance_ids); + + $e->disconnect; + undef; +} + +__PACKAGE__->register_method( + "method" => "receive_items_by_id", + "api_name" => "open-ils.serial.items.receive_by_id", + "stream" => 1, + "signature" => { + "desc" => "Given sitem IDs, just set their date_received to now()", + "params" => [ + {"desc" => "Authtoken", "type" => "string"}, + {"desc" => "Serial Item IDs", "type" => "array"}, + ], + "return" => { + "desc" => "Stream of updated items", "type" => "object" + } + } +); + +sub receive_items_by_id { + my ($self, $client, $auth, $id_list) = @_; + + my $e = new_editor("authtoken" => $auth, "xact" => 1); + return $e->die_event unless $e->checkauth; + + # XXX permissions + + # for now this function doesn't do nearly enough. simply sets + # date_received to now() + + my @results = (); + foreach (@$id_list) { + my $sitem = $e->retrieve_serial_item($_) or return $e->die_event; + + $sitem->date_received("now"); + $e->update_serial_item($sitem) or return $e->die_event; + + push @results, $sitem; + } + + $e->commit; + $client->respond($_) foreach @results; + undef; +} + 1; diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd index f61896e12b..f9d41a70a4 100644 --- a/Open-ILS/web/opac/locale/en-US/lang.dtd +++ b/Open-ILS/web/opac/locale/en-US/lang.dtd @@ -316,6 +316,8 @@ + + @@ -747,6 +749,19 @@ + + + + + + + + + + + + + @@ -852,6 +867,11 @@ + + + + + @@ -1576,6 +1596,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/xul/staff_client/chrome/content/cat/opac.js b/Open-ILS/xul/staff_client/chrome/content/cat/opac.js index cdbf7e9e5e..e07584b86e 100644 --- a/Open-ILS/xul/staff_client/chrome/content/cat/opac.js +++ b/Open-ILS/xul/staff_client/chrome/content/cat/opac.js @@ -538,6 +538,28 @@ function open_marc_editor(rec, label) { }; } +function serials_mgmt_new_tab() { + try { + /* XXX should the following be put into a function somewhere? the gist + * of this setting up of content_params seems to be duplicated all + * over the place. + */ + var content_params = {"session": ses(), "authtime": ses("authtime")}; + ["url_prefix", "new_tab", "set_tab", "close_tab", "new_patron_tab", + "set_patron_tab", "volume_item_creator", "get_new_session", + "holdings_maintenance_tab", "set_tab_name", "open_chrome_window", + "url_prefix", "network_meter", "page_meter", "set_statusbar", + "set_help_context" + ].forEach(function(k) { content_params[k] = xulG[k]; }); + + xulG.new_tab( + xulG.url_prefix(urls.XUL_SERIAL_RECORD_ENTRY), {}, content_params + ); + } catch (E) { + g.error.sdump('D_ERROR', E); + } +} + function bib_in_new_tab() { try { var url = browser_frame.contentWindow.g.browser.controller.view.browser_browser.contentWindow.wrappedJSObject.location.href; @@ -565,6 +587,30 @@ function bib_in_new_tab() { } } +function batch_receive_in_new_tab() { + try { + var content_params = {"session": ses(), "authtime": ses("authtime")}; + + ["url_prefix", "new_tab", "set_tab", "close_tab", "new_patron_tab", + "set_patron_tab", "volume_item_creator", "get_new_session", + "holdings_maintenance_tab", "set_tab_name", "open_chrome_window", + "url_prefix", "network_meter", "page_meter", "set_statusbar", + "set_help_context" + ].forEach(function(k) { content_params[k] = xulG[k]; }); + + xulG.new_tab( + xulG.url_prefix(urls.XUL_SERIAL_BATCH_RECEIVE) + + "?docid=" + window.escape(docid), { + "tab_name": $("offlineStrings").getString( + "menu.cmd_serial_batch_receive.tab" + ) + }, content_params + ); + } catch (E) { + g.error.sdump("D_ERROR", E); + } +} + function remove_me() { var url = xulG.url_prefix( urls.XUL_BIB_BRIEF ) + '?docid=' + window.escape(docid); dump('removing ' + url + '\n'); diff --git a/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul b/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul index dd26a6dfcb..0c9396f819 100644 --- a/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul +++ b/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul @@ -74,6 +74,7 @@ + diff --git a/Open-ILS/xul/staff_client/chrome/content/main/constants.js b/Open-ILS/xul/staff_client/chrome/content/main/constants.js index 070cf40b85..38d096cfe7 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/constants.js +++ b/Open-ILS/xul/staff_client/chrome/content/main/constants.js @@ -458,5 +458,6 @@ var urls = { 'EG_WEB_BASE' : '/eg', 'XUL_LOCAL_ADMIN_BASE' : '/xul/server/admin', 'XUL_REPORTS' : '/reports/oils_rpt.xhtml', - 'EG_ACQ_PO_VIEW' : '/eg/acq/po/view' + 'EG_ACQ_PO_VIEW' : '/eg/acq/po/view', + 'XUL_SERIAL_BATCH_RECEIVE': '/xul/server/serial/batch_receive.xul' } diff --git a/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties b/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties index 07c9f4a042..d6d4acd632 100644 --- a/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties +++ b/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties @@ -233,6 +233,7 @@ menu.cmd_acq_new_brief_record.tab=New Brief Record menu.cmd_acq_po.tab=Purchase Orders menu.cmd_acq_user_requests.tab=Patron Requests menu.cmd_acq_claim_eligible.tab=Claim-Ready Items +menu.cmd_serial_batch_receive.tab=Batch Receive menu.cmd_booking_resource.tab=Resources menu.cmd_booking_reservation.tab=Reservations menu.cmd_booking_reservation_pickup.tab=Reservation Pickup diff --git a/Open-ILS/xul/staff_client/server/locale/en-US/serial.properties b/Open-ILS/xul/staff_client/server/locale/en-US/serial.properties index 0abd318624..45cef07f53 100644 --- a/Open-ILS/xul/staff_client/server/locale/en-US/serial.properties +++ b/Open-ILS/xul/staff_client/server/locale/en-US/serial.properties @@ -53,3 +53,13 @@ staff.serial.manage_subs.delete_ssub.confirm=Are you sure you would like to dele staff.serial.manage_subs.delete_ssub.confirm.plural=Are you sure you would like to delete these %1$s subscriptions? staff.serial.manage_subs.delete_ssub.title=Delete Subscriptions? staff.serial.manage_subs.delete_ssub.override=Override Delete Failure? Doing so will delete all related data as well! +batch_receive.bib_lookup.empty=Enter a search term. +batch_receive.bib_lookup.multiple=Multiple matching records found. Please use a more specific identifier, or use the catalog to find the exact record you want. +batch_receive.bib_lookup.not_found=No matching records found with any subscriptions attached. +batch_receive.issuance_lookup.error=Problem retrieving issuances related to subscription. +batch_receive.issuance_lookup.none=There are no receivable issuances. +batch_receive.item_lookup.none=Could not retrieve receivable items for this issuance. +batch_receive.autogen_barcodes.questionable=There are already barcodes entered further down the list than the one you just entered.\nFill the intervening fields with auto-generated barcodes? +batch_receive.autogen_barcodes.remove=Clear the barcodes that have already been auto-generated? +batch_receive.none=[None] +batch_receive.apply=Apply diff --git a/Open-ILS/xul/staff_client/server/serial/batch_receive.js b/Open-ILS/xul/staff_client/server/serial/batch_receive.js new file mode 100644 index 0000000000..fce90acba5 --- /dev/null +++ b/Open-ILS/xul/staff_client/server/serial/batch_receive.js @@ -0,0 +1,711 @@ +dojo.require("dojo.cookie"); +dojo.require("dojo.date.locale"); +dojo.require("dojo.date.stamp"); +dojo.require("openils.Util"); +dojo.require("openils.CGI"); + +var authtoken; +var batch_receiver; + +String.prototype.trim = function() {return this.replace(/^\s*(.+)\s*$/,"$1");} + +/** + * hard_empty() is needed because dojo.empty() doesn't seem to work on + * XUL nodes. This also means that dojo.place() with a position argument of + * "only" doesn't do what it should, but calling hard_empty() on the refnode + * first will do the trick. + */ +function hard_empty(node) { + if (typeof(node) == "string") + node = dojo.byId(node); + if (node) + dojo.forEach(node.childNodes, dojo.destroy); +} + +function hide(e) { + if (typeof(e) == "string") e = dojo.byId(e); + openils.Util.addCSSClass(e, "hideme"); +} + +function show(e) { + if (typeof(e) == "string") e = dojo.byId(e); + openils.Util.removeCSSClass(e, "hideme"); +} + +function busy(on) { + if (typeof(busy._window) == "undefined") + busy._window = dojo.query("window")[0]; + busy._window.style.cursor = on ? "wait" : "auto"; +} + +function S(k) { + return dojo.byId("serialStrings").getString("batch_receive." + k). + replace("\\n", "\n"); +} + +function T(s) { return document.createTextNode(s); } +function D(s) {return s ? openils.Util.timeStamp(s, {"selector":"date"}) : "";} +function node_by_name(s, ctx) {return dojo.query("[name='" + s + "']", ctx)[0];} + +function num_sort(a, b) { + [a, b] = [Number(a), Number(b)]; + return a > b ? 1 : (a < b ? -1 : 0); +} + +function BatchReceiver() { + var self = this; + + this._init = function(bib_id) { + hide("batch_receive_sub"); + hide("batch_receive_entry"); + hide("batch_receive_bibdata_bits"); + hide("batch_receive_sub_bits"); + hide("batch_receive_issuance_bits"); + hide("batch_receive_issuance"); + + dojo.byId("bib_lookup_submit").disabled = false; + dojo.byId("bib_search_term").value = ""; + + if (!bib_id) { + show("batch_receive_bib"); + dojo.byId("bib_search_term").focus(); + } + + if (!this.entry_tbody) { + this.entry_tbody = dojo.byId("entry_tbody"); + this.template = this.entry_tbody.removeChild( + dojo.byId("entry_template") + ); + } + + this._clear_entry_batch_row(); + + this._copy_loc_by_lib = {}; + + /* empty the entry receiving table if we're starting over */ + if (this.item_cache) { + for (var id in this.item_cache) + this.finish_receipt(this.item_cache[id]); + } + + this.rows = {}; + this.item_cache = {}; + + if (bib_id) + this.bib_lookup(bib_id, null, true); + + busy(false); + }; + + this._clear_entry_batch_row = function() { + dojo.forEach( + dojo.byId("entry_batch_row").childNodes, + function(node) { + if (node.nodeType == 1 && + node.getAttribute("name") != "barcode") + hard_empty(node); + } + ); + }; + + this._show_bibdata_bits = function() { + hard_empty("title_here"); + dojo.byId("title_here").appendChild(T(this.bibdata.mvr.title())); + hard_empty("author_here"); + + if (this.bibdata.mvr.author()) { + dojo.byId("author_here").appendChild(T(this.bibdata.mvr.author())); + show("author_here_holder"); + } else { + hide("author_here_holder"); + } + + show("batch_receive_bibdata_bits"); + }; + + this._sub_label = function(sub) { + /* XXX use a formatting string from serial.properties */ + return sub.id() + ": (" + sub.owning_lib().shortname() + ") " + + D(sub.start_date()) + " - " + D(sub.end_date()); + }; + + this._show_sub_bits = function() { + hard_empty("sublabel_here"); + dojo.place( + T(this._sub_label(this.sub)), + "sublabel_here", + "only" + ); + hide("batch_receive_sub"); + show("batch_receive_sub_bits"); + }; + + this._show_issuance_bits = function() { + hide("batch_receive_issuance"); + hard_empty("issuance_label_here"); + dojo.place( + T(this.issuance.label()), + "issuance_label_here", + "only" + ); + show("batch_receive_issuance_bits"); + } + + this._get_receivable_issuances = function() { + var issuances = []; + + busy(true); + try { + fieldmapper.standardRequest( + ["open-ils.serial", "open-ils.serial.issuances.receivable"], { + "params": [authtoken, this.sub.id()], + "async": false, + "onresponse": function(r) { + if (r = openils.Util.readResponse(r)) + issuances.push(r); + } + } + ); + } catch (E) { + alert(E); + } + busy(false); + + return issuances; + }; + + this._build_circ_mod_dropdown = function() { + if (!this._built_circ_mod_dropdown) { + var menulist = dojo.create("menulist"); + var menupopup = dojo.create("menupopup", null, menulist, "only"); + dojo.create( + "menuitem", {"value": 0, "label": S("none")}, + menupopup, "first" + ); + + var mods = []; + fieldmapper.standardRequest( + ["open-ils.circ", "open-ils.circ.circ_modifier.retrieve.all"], { + "params": [], + "async": false, + "onresponse": function(r) { + if (mods = openils.Util.readResponse(r)) { + mods.forEach( + function(mod) { + dojo.create( + "menuitem", { + "value": mod, "label": mod + }, menupopup, "last" + ); + } + ); + } + } + } + ); + if (!mods.length) { + /* in this case, discard menulist and menupopup */ + this._built_circ_mod_dropdown = + dojo.create("description", {"value": "-"}); + } else { + this._built_circ_mod_dropdown = menulist; + } + } + + return dojo.clone(this._built_circ_mod_dropdown); + }; + + this._extend_circ_mod_for_batch = function(control) { + dojo.create( + "menuitem", {"value": -1, "label": "---"}, + dojo.query("menupopup", control)[0], + "first" + ); + return control; + }; + + this._build_copy_loc_dropdown = function(locs, add_unset_value) { + var menulist = dojo.create("menulist"); + var menupopup = dojo.create("menupopup", null, menulist, "only"); + + if (add_unset_value) { + dojo.create( + "menuitem", {"value": -1, "label": "---"}, menupopup, "first" + ); + } + + locs.forEach( + function(loc) { + dojo.create( + "menuitem", { + "value": loc.id(), + "label": "(" + loc.owning_lib().shortname() + ") " + + loc.name() /* XXX i18n */ + }, menupopup, "last" + ); + } + ); + + return menulist; + }; + + this._get_copy_locs_for_lib = function(lib) { + if (!this._copy_loc_by_lib[lib]) { + fieldmapper.standardRequest( + ["open-ils.circ", "open-ils.circ.copy_location.retrieve.all"], { + "params": [lib, false, true], + "async": false, + "onresponse": function(r) { + if (locs = openils.Util.readResponse(r)) + self._copy_loc_by_lib[lib] = locs; + } + } + ); + } + + return this._copy_loc_by_lib[lib]; + }; + + this._build_receive_toggle = function(item) { + return dojo.create( + "checkbox", { + "oncommand": function(ev) { + self._disable_row(item.id(), !ev.target.checked); + }, + "checked": "true" + } + ); + } + + this._disable_row = function(item_id, disabled) { + var row = this.rows[item_id]; + dojo.query("textbox,menulist", row).forEach( + function(element) { element.disabled = disabled; } + ); + }; + + this._row_disabled = function(row) { + if (typeof(row) == "string") row = this.rows[row]; + return !dojo.query("checkbox", row)[0].checked; + }; + + this._row_field_value = function(row, field, value) { + if (typeof(row) == "string") row = this.rows[row]; + + var node = dojo.query("*", node_by_name(field, row))[0]; + + if (typeof(value) == "undefined") + return node.value; + else + node.value = value; + } + + this._user_wants_autogen = function() { + return dojo.byId("autogen_barcodes").checked; + }; + + this._get_autogen_potentials = function(item_id) { + var hit_a_wall = false; + + return [openils.Util.objectProperties(this.rows).sort(num_sort).filter( + function(id) { + if (hit_a_wall) { + return false; + } else if (id <= item_id || self._row_disabled(id)) { + return false; + } else if (self._row_field_value(id, "barcode")) { + hit_a_wall = true; + return false; + } else { + return true; + } + } + ), hit_a_wall]; + }; + + this._prepare_autogen_control = function() { + dojo.attr("autogen_barcodes", + "command", function(ev) { + if (!ev.target.checked) { + var list = self._have_autogen_barcodes(); + if (list.length && confirm(S("autogen_barcodes.remove"))) { + list.forEach( + function(id) { + self._row_field_value(id, "barcode", ""); + self.rows[id]._has_autogen_barcode = false; + } + ); + } + } + } + ); + }; + + this._have_autogen_barcodes = function() { + var list = []; + for (var id in this.rows) + if (this.rows[id]._has_autogen_barcode) list.push(id); + return list; + }; + + this._set_all_enabled_rows = function(key, value) { + /* do NOT do trimming here, set whitespace as is. */ + for (var id in this.rows) { + if (!this._row_disabled(id)) + this._row_field_value(id, key, value); + } + }; + + this.bib_lookup = function(bib_search_term, evt, is_actual_id) { + if (evt && evt.keyCode != 13) return; + + if (!bib_search_term) { + var bib_search_term = dojo.byId("bib_search_term").value.trim(); + if (!bib_search_term.length) { + alert(S("bib_lookup.empty")); + return; + } + } + + hide("batch_receive_sub"); + hide("batch_receive_entry"); + + busy(true); + dojo.byId("bib_lookup_submit").disabled = true; + fieldmapper.standardRequest( + ["open-ils.serial", + "open-ils.serial.biblio.record_entry.by_identifier.atomic"], { + "params": [ + bib_search_term, { + "require_subscriptions": true, + "add_mvr": true, + "is_actual_id": is_actual_id + } + ], + "async": false, + "oncomplete": function(r) { + /* These two things better come before readResponse(), which + * can throw exceptions. */ + busy(false); + dojo.byId("bib_lookup_submit").disabled = false; + + var list = openils.Util.readResponse(r, false, true); + if (list && list.length) { + if (list.length > 1) { + /* XXX TODO just let the user pick one from a list, + * although this circumstance seems really + * unlikely. It just can't happen for TCN, and + * wouldn't be likely for ISxN or UPC... ? */ + alert(S("bib_lookup.multiple")); + } else { + self.bibdata = list[0]; + self._show_bibdata_bits(); + self.choose_subscription(); + } + } else { + alert(S("bib_lookup.not_found")); + if (is_actual_id) { + self._init(); + } else { + dojo.byId("bib_search_term").reset(); + dojo.byId("bib_search_term").focus(); + } + } + } + } + ); + }; + + this.choose_subscription = function() { + hide("batch_receive_bib"); + hide("batch_receive_entry"); + hide("batch_receive_sub_bits"); + hide("batch_receive_issuance"); + + var subs = this.bibdata.bre.subscriptions(); + + if (subs.length > 1) { + var menulist = dojo.create("menulist", {"id": "sub_chooser"}); + var menupopup = dojo.create("menupopup", {}, menulist, "only"); + + this.bibdata.bre.subscriptions().forEach( + function(sub) { + dojo.create( + "menuitem", { + "label": self._sub_label(sub), + "value": sub.id() + }, menupopup, "last" + ); + } + ); + + hard_empty(dojo.byId("sub_chooser_here")); + + dojo.place(menulist, dojo.byId("sub_chooser_here"), "only"); + show("batch_receive_sub"); + } else { + this.choose_issuance(subs[0]); + } + }; + + this.choose_issuance = function(sub) { + hide("batch_receive_bib"); + hide("batch_receive_entry"); + hide("batch_receive_sub"); + + if (typeof(sub) == "undefined") { /* sub chosen from menu */ + var sub_id = dojo.byId("sub_chooser").value; + this.sub = this.bibdata.bre.subscriptions().filter( + function(o) { return o.id() == sub_id; } + )[0]; + } else { /* only one sub possible, passed in directly */ + this.sub = sub; + } + + this._show_sub_bits(); + + this.issuances = this._get_receivable_issuances(); /* sync */ + + if (this.issuances.length > 1) { + var menulist = dojo.create("menulist", {"id": "issuance_chooser"}); + var menupopup = dojo.create("menupopup", {}, menulist, "only"); + + this.issuances.sort( + function(a, b) { + if (a.date_published() > b.date_published()) return 1; + else if (b.date_published() > a.date_published()) return -1; + else return 0; + } + ).forEach( + function(issuance) { + dojo.create( + "menuitem", { + "label": issuance.label(), + "value": issuance.id() + }, menupopup, "last" + ); + } + ); + + hard_empty("issuance_chooser_here"); + dojo.place(menulist, dojo.byId("issuance_chooser_here"), "only"); + + show("batch_receive_issuance"); + } else if (this.issuances.length) { + this.load_entry_form(this.issuances[0]); + } else { + alert(S("issuance_lookup.none")); + this._init(); + } + + }; + + this.load_entry_form = function(issuance) { + if (typeof(issuance) == "undefined") { + var issuance_id = dojo.byId("issuance_chooser").value; + this.issuance = this.issuances.filter( + function(o) { return o.id() == issuance_id; } + )[0]; + } else { + this.issuance = issuance; + } + + this._show_issuance_bits(); + this._prepare_autogen_control(); + + busy(true); + + fieldmapper.standardRequest( + ["open-ils.serial", + "open-ils.serial.items.receivable.by_issuance.atomic"], { + "params": [authtoken, this.issuance.id()], + "async": true, + "onresponse": function(r) { + busy(false); + + if (list = openils.Util.readResponse(r, false, true)) { + + if (list.length) { + busy(true); + show("form_holder"); + + list.forEach(function(o) {self.add_entry_row(o);}); + if (list.length > 1) { + self.build_batch_entry_row(); + show("batch_receive_entry"); + } + + busy(false); + } else { + alert(S("item_lookup.none")); + if (self.issuances.length) self.choose_issuance(); + else self._init(); + } + } + } + } + ); + + }; + + this.build_batch_entry_row = function() { + var row = dojo.byId("entry_batch_row"); + + this.batch_controls = {}; + + node_by_name("note", row).appendChild( + this.batch_controls.note = dojo.create("textbox", {"size": 20}) + ); + + node_by_name("copy_loc", row).appendChild( + this.batch_controls.copy_loc = this._build_copy_loc_dropdown( + /* XXX is 1 really the right value below? */ + this._get_copy_locs_for_lib(1), + true /* add_unset_value */ + ) + ); + + node_by_name("circ_mod", row).appendChild( + this.batch_controls.circ_mod = this._extend_circ_mod_for_batch( + this._build_circ_mod_dropdown() + ) + ); + + node_by_name("price", row).appendChild( + this.batch_controls.price = dojo.create("textbox", {"size": 9}) + ); + + node_by_name("apply", row).appendChild( + dojo.create("button", { + "label": S("apply"), + "oncommand": function() { self.apply_batch_values(); } + }) + ); + }; + + this.apply_batch_values = function() { + var row = dojo.byId("entry_batch_row"); + + for (var key in this.batch_controls) { + var value = this.batch_controls[key].value; + if (value != "" && value != -1) + this._set_all_enabled_rows(key, value); + } + }; + + this.add_entry_row = function(item) { + this.item_cache[item.id()] = item; + var row = this.rows[item.id()] = dojo.clone(this.template); + + function n(s) { return node_by_name(s, row); } /* typing saver */ + + n("holding_lib").appendChild( + T(item.stream().distribution().holding_lib().shortname()) + ); + + n("barcode").appendChild( + dojo.create( + "textbox", { + "size": 15, + "tabindex": 10000 + Number(item.id()), /* is this right? */ + "onchange": function() { + self.autogen_if_appropriate(this, item.id()); + } + } + ) + ); + + n("copy_loc").appendChild( + this._build_copy_loc_dropdown( + this._get_copy_locs_for_lib( + item.stream().distribution().holding_lib().id() + ) + ) + ); + + n("note").appendChild(dojo.create("textbox", {"size": 20})); + n("circ_mod").appendChild(this._build_circ_mod_dropdown()); + n("price").appendChild(dojo.create("textbox", {"size": 9})); + n("receive").appendChild(this._build_receive_toggle(item)); + + this.entry_tbody.appendChild(row); + }; + + this.receive = function() { + var recv_ids = []; + for (var id in this.rows) { + /* XXX TODO: get field values, send to ML, + * and yes do trimming here. */ + if (!this._row_disabled(id)) recv_ids.push(id); + } + + busy(true); + fieldmapper.standardRequest( + ["open-ils.serial", "open-ils.serial.items.receive_by_id"], { + "params": [authtoken, recv_ids], + "async": true, + "oncomplete": function(r) { + try { + while (item = openils.Util.readResponse(r)) + self.finish_receipt(item); + } catch (E) { + alert(E); + } + busy(false); + } + } + ); + }; + + this.finish_receipt = function(item) { + dojo.destroy(this.rows[item.id()]); + delete this.rows[item.id()]; + delete this.item_cache[item.id()]; + }; + + this.autogen_if_appropriate = function(textbox, item_id) { + if (this._user_wants_autogen() && textbox.value) { + var [list, question] = this._get_autogen_potentials(item_id); + if (list.length) { + if (question && !confirm(S("autogen_barcodes.questionable"))) + return; + + busy(true); + try { + fieldmapper.standardRequest( + ["open-ils.cat", "open-ils.cat.item.barcode.autogen"], { + "params": [authtoken, textbox.value, list.length], + "async": false, + "onresponse": function(r) { + r = openils.Util.readResponse(r, false, true); + if (r) { + for (var i = 0; i < r.length; i++) { + var row = self.rows[list[i]]; + self._row_field_value( + row, "barcode", r[i] + ); + row._has_autogen_barcode = true; + } + } + } + } + ); + } catch (E) { + alert(E); + } + busy(false); + } /* do nothing for empty list */ + } + }; + + this._init.apply(this, arguments); +} + +function my_init() { + var cgi = new openils.CGI(); + + authtoken = (typeof ses == "function" ? ses() : 0) || + cgi.param("ses") || dojo.cookie("ses"); + + batch_receiver = new BatchReceiver(cgi.param("docid") || null); +} diff --git a/Open-ILS/xul/staff_client/server/serial/batch_receive.xul b/Open-ILS/xul/staff_client/server/serial/batch_receive.xul new file mode 100644 index 0000000000..98c90f99cf --- /dev/null +++ b/Open-ILS/xul/staff_client/server/serial/batch_receive.xul @@ -0,0 +1,32 @@ + + + + + +]> + + + + + + + + + + +