Serials: improve the alternative batch receive interface for the
authorsenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Wed, 1 Sep 2010 20:10:04 +0000 (20:10 +0000)
committersenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Wed, 1 Sep 2010 20:10:04 +0000 (20:10 +0000)
barcode-heavy, one unit per item workflow. Support setting call numbers
at receive time, effectively making it possible to associate call numbers
with issuances instead of associating them with distributions.

Other bugfixes/tweaks to the same interface.

git-svn-id: svn://svn.open-ils.org/ILS/trunk@17428 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/src/perlmods/OpenILS/Application/Serial.pm
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/xul/staff_client/server/locale/en-US/serial.properties
Open-ILS/xul/staff_client/server/serial/batch_receive.js
Open-ILS/xul/staff_client/server/serial/batch_receive_overlay.xul
Open-ILS/xul/staff_client/server/skin/serial.css

index bee7f1c..4b64407 100644 (file)
@@ -878,22 +878,34 @@ sub unitize_items {
 }
 
 __PACKAGE__->register_method(
-    method    => "receive_items_one_unit_per",
-    api_name  => "open-ils.serial.receive_items.one_unit_per",
-    stream => 1,
-    api_level => 1,
-    argc      => 1,
-    signature => {
-        desc     => "Marks items in a list as received, creates a new unit for each item if any unit is fleshed on",
-        "params" => [ {
-                 name => "items",
-                 desc => "array of serial items, possibly fleshed with units and definitely fleshed with stream->distribution",
-                 type => "array"
+    "method" => "receive_items_one_unit_per",
+    "api_name" => "open-ils.serial.receive_items.one_unit_per",
+    "stream" => 1,
+    "api_level" => 1,
+    "argc" => 3,
+    "signature" => {
+        "desc" => "Marks items in a list as received, creates a new unit for each item if any unit is fleshed on",
+        "params" => [
+            {
+                 "name" => "auth",
+                 "desc" => "authtoken",
+                 "type" => "string"
+            },
+            {
+                 "name" => "items",
+                 "desc" => "array of serial items, possibly fleshed with units and definitely fleshed with stream->distribution",
+                 "type" => "array"
+            },
+            {
+                "name" => "record",
+                "desc" => "id of bib record these items are associated with
+                    (XXX could/should be derived from items)",
+                "type" => "number"
             }
         ],
         "return" => {
-            desc => "The item ID for each item successfully received",
-            type => "int"
+            "desc" => "The item ID for each item successfully received",
+            "type" => "int"
         }
     }
 );
@@ -907,7 +919,7 @@ sub receive_items_one_unit_per {
     # method names that point to this function can be repointed at
     # unitize_items()
 
-    my ($self, $client, $auth, $items) = @_;
+    my ($self, $client, $auth, $items, $record) = @_;
 
     my $e = new_editor("authtoken" => $auth, "xact" => 1);
     return $e->die_event unless $e->checkauth;
@@ -933,7 +945,7 @@ sub receive_items_one_unit_per {
             my $user_unit = $item->unit;
 
             # get a unit based on associated template
-            my $template_unit = _build_unit($e, $sdist, "receive");
+            my $template_unit = _build_unit($e, $sdist, "receive", 1);
             if ($U->event_code($template_unit)) {
                 $e->rollback;
                 $template_unit->{"note"} = "Item ID: " . $item->id;
@@ -947,6 +959,39 @@ sub receive_items_one_unit_per {
                 }
             }
 
+            if ($user_unit->call_number) {
+                # call_number passed in will actually be a string representing
+                # a call_number label, not an actual acn object or even an ID.
+                # Therefore we must lookup the call_number requested, or
+                # create a new one if it does not exist for the given lib.
+
+                my $existing = $e->search_asset_call_number({
+                    "owning_lib" => $sdist->holding_lib->id,
+                    "label" => $user_unit->call_number,
+                    "record" => $record,
+                    "deleted" => "f"
+                }) or return $e->die_event;
+
+                if (@$existing) {
+                    $user_unit->call_number($existing->[0]->id);
+                } else {
+                    return $e->die_event unless
+                        $e->allowed("CREATE_VOLUME", $sdist->holding_lib->id);
+
+                    my $acn = new Fieldmapper::asset::call_number;
+
+                    $acn->creator($user_id);
+                    $acn->editor($user_id);
+                    $acn->record($record);
+                    $acn->label($user_unit->call_number);
+                    $acn->owning_lib($sdist->holding_lib->id);
+
+                    $e->create_asset_call_number($acn) or return $e->die_event;
+
+                    $user_unit->call_number($e->data->id);
+                }
+            }
+
             # set the incontrovertibles on the unit
             $user_unit->edit_date("now");
             $user_unit->create_date("now");
@@ -992,6 +1037,7 @@ sub _build_unit {
     my $editor = shift;
     my $sdist = shift;
     my $mode = shift;
+    my $skip_call_number = shift;
 
     my $attr = $mode . '_unit_template';
     my $template = $editor->retrieve_asset_copy_template($sdist->$attr) or
@@ -1011,11 +1057,14 @@ sub _build_unit {
     $unit->creator($editor->requestor->id);
     $unit->editor($editor->requestor->id);
 
-    $attr = $mode . '_call_number';
-    my $cn = $sdist->$attr or
-        return new OpenILS::Event("SERIAL_DISTRIBUTION_HAS_NO_CALL_NUMBER");
+    unless ($skip_call_number) {
+        $attr = $mode . '_call_number';
+        my $cn = $sdist->$attr or
+            return new OpenILS::Event("SERIAL_DISTRIBUTION_HAS_NO_CALL_NUMBER");
+
+        $unit->call_number($cn);
+    }
 
-    $unit->call_number($cn);
     $unit->barcode('AUTO');
     $unit->sort_key('');
     $unit->summary_contents('');
index ff0b626..51b4b43 100644 (file)
 <!ENTITY staff.serial.batch_receive.org_unit "Org Unit">
 <!ENTITY staff.serial.batch_receive.barcode "Barcode">
 <!ENTITY staff.serial.batch_receive.circ_modifier "Circ Modifier">
+<!ENTITY staff.serial.batch_receive.call_number "Call Number">
 <!ENTITY staff.serial.batch_receive.note "Note">
 <!ENTITY staff.serial.batch_receive.location "Copy Location">
 <!ENTITY staff.serial.batch_receive.price "Price">
 <!ENTITY staff.serial.batch_receive.recieve_selected "Receive Selected Items">
 <!ENTITY staff.serial.batch_receive.start_over "Start Over">
 <!ENTITY staff.serial.batch_receive.start_over.accesskey "O">
+<!ENTITY staff.serial.batch_receive.with_units "Create Units For Received Items">
+<!ENTITY staff.serial.batch_receive.with_units.accesskey "U">
 
 <!ENTITY staff.survey.wizard.page1 "Initial Settings">
 <!ENTITY staff.survey.wizard.page2 "Add Questions for Survey:">
index f63f505..79f3dcb 100644 (file)
@@ -64,3 +64,6 @@ batch_receive.autogen_barcodes.remove=Clear the barcodes that have already been
 batch_receive.none=[None]
 batch_receive.apply=Apply
 batch_receive.receive_time_note=Receive-time Note
+batch_receive.cn_for_lib=Do you want to use this call number at %1$s?\nIt doesn't exist there, and it will have to be created.
+batch_receive.missing_units=You have not provided barcodes and call numbers for all of the selected items.  Choose OK to receive those items anyway, or choose Cancel to supply the missing information.
+batch_receive.missing_cn=You cannot assign a barcode without selecting a call number. Please correct the non-conforming units.
index a6b0db0..a0bb486 100644 (file)
@@ -1,10 +1,12 @@
 dojo.require("dojo.cookie");
 dojo.require("dojo.date.locale");
 dojo.require("dojo.date.stamp");
+dojo.require("dojo.string");
 dojo.require("openils.Util");
+dojo.require("openils.User");
 dojo.require("openils.CGI");
+dojo.require("openils.PermaCrud");
 
-var authtoken;
 var batch_receiver;
 
 String.prototype.trim = function() {return this.replace(/^\s*(.+)\s*$/,"$1");}
@@ -18,8 +20,19 @@ String.prototype.trim = function() {return this.replace(/^\s*(.+)\s*$/,"$1");}
 function hard_empty(node) {
     if (typeof(node) == "string")
         node = dojo.byId(node);
-    if (node)
-        dojo.forEach(node.childNodes, dojo.destroy);
+
+    if (node && node.childNodes.length > 0) {
+        dojo.forEach(
+            node.childNodes,
+            function(c) {
+                if (c) {
+                    if (c.childNodes.length > 0)
+                        dojo.forEach(c.childNodes, hard_empty);
+                    dojo.destroy(c);
+                }
+            }
+        );
+    }
 }
 
 function hide(e) {
@@ -43,6 +56,11 @@ function S(k) {
         replace("\\n", "\n");
 }
 
+function F(k, args) {
+    return dojo.byId("serialStrings").
+        getFormattedString("batch_receive." + k, args).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];}
@@ -55,7 +73,13 @@ function num_sort(a, b) {
 function BatchReceiver() {
     var self = this;
 
-    this._init = function(bib_id) {
+    this.init = function(authtoken, bib_id) {
+        if (authtoken) {
+            this.user = new openils.User({"authtoken": authtoken});
+            this.pcrud = new openils.PermaCrud({"authtoken": authtoken});
+            this.authtoken = authtoken;
+        }
+
         hide("batch_receive_sub");
         hide("batch_receive_entry");
         hide("batch_receive_bibdata_bits");
@@ -80,6 +104,8 @@ function BatchReceiver() {
 
         this._clear_entry_batch_row();
 
+        this._call_number_cache = null;
+        this._prepared_call_number_controls = {};
         this._location_by_lib = {};
 
         /* empty the entry receiving table if we're starting over */
@@ -165,7 +191,7 @@ function BatchReceiver() {
         try {
             fieldmapper.standardRequest(
                 ["open-ils.serial", "open-ils.serial.issuances.receivable"], {
-                    "params": [authtoken, this.sub.id()],
+                    "params": [this.authtoken, this.sub.id()],
                     "async": false,
                     "onresponse": function(r) {
                         if (r = openils.Util.readResponse(r))
@@ -281,6 +307,66 @@ function BatchReceiver() {
         return this._location_by_lib[lib];
     };
 
+    this._build_call_number_control = function(item) {
+        /* In any case, give a dropdown of call numbers related to the
+         * same bre as the subscription relates to. */
+        if (!this._call_number_cache) {
+            this._call_number_cache = this.pcrud.search(
+                "acn", {
+                    "record": this.sub.record_entry()
+                }, {
+                    "order_by": {"acn": "label"},   /* XXX wrong sorting? */
+                }
+            );
+        }
+
+        if (typeof item == "undefined") {
+            /* In this case, no further limiting of call numbers for now,
+             * although ideally it might be nice to limit to call numbers
+             * with owning_lib matching the holding_lib of the distribs
+             * that ultimately relate to the items. */
+
+            var menulist = dojo.create("menulist", {
+                "editable": "true", "className": "cn"
+            });
+            var menupopup = dojo.create("menupopup", null, menulist, "only");
+            this._call_number_cache.forEach(
+                function(cn) {
+                    dojo.create(
+                        "menuitem", {
+                            "value": cn.id(), "label": cn.label()
+                        }, menupopup, "last"
+                    );
+                }
+            );
+            return menulist;
+        } else {
+            /* In this case, limit call numbers by owning_lib matching
+             * distributions's holding_lib. */
+
+            var lib = item.stream().distribution().holding_lib().id();
+            if (!this._prepared_call_number_controls[lib]) {
+                var menulist = dojo.create("menulist", {
+                    "editable": "true", "className": "cn"
+                });
+                var menupopup = dojo.create("menupopup", null, menulist,"only");
+                this._call_number_cache.filter(
+                    function(cn) { return cn.owning_lib() == lib; }
+                ).forEach(
+                    function(cn) {
+                        dojo.create(
+                            "menuitem", {
+                                "value": cn.id(), "label": cn.label()
+                            }, menupopup, "last"
+                        );
+                    }
+                );
+                this._prepared_call_number_controls[lib] = menulist;
+            }
+            return dojo.clone(this._prepared_call_number_controls[lib]);
+        }
+    };
+
     this._build_receive_toggle = function(item) {
         return dojo.create(
             "checkbox", {
@@ -363,11 +449,58 @@ function BatchReceiver() {
         return list;
     };
 
+    this._cn_exists_but_not_for_lib = function(lib, value) {
+        var exists = this._call_number_cache.filter(
+            function(cn) { return cn.label() == value }
+        );
+        var for_lib = exists.filter(
+            function(cn) { return cn.owning_lib() == lib; }
+        );
+        return (exists.length && !for_lib.length);
+    };
+
+    this._call_number_confirm_for_lib = function(lib, value) {
+        /* XXX Right now, this method will ask the user if they're serious if
+         * they apply an _existing_ (somewhere) call number to an item
+         * going to a library where that call number _doesn't_ exist,but it
+         * won't say anything if the user enters a brand new call number.
+         * This may not be ideal, and can be reworked later. */
+        if (!this._has_confirmed_cn_for)
+            this._has_confirmed_cn_for = {};
+
+        if (typeof(this._has_confirmed_cn_for[lib.id()]) == "undefined") {
+            if (this._cn_exists_but_not_for_lib(lib.id(), value)) {
+                this._has_confirmed_cn_for[lib.id()] = confirm(
+                    F("cn_for_lib", [lib.shortname()])
+                );
+            } else {
+                this._has_confirmed_cn_for[lib.id()] = true;
+            }
+        }
+
+        return this._has_confirmed_cn_for[lib.id()];
+    }
+
+    this._confirm_row_field_application = function(id, key, value) {
+        if (key == "call_number") { /* XXX make a dispatch table so we can do
+                                       this for other fields too */
+            return this._call_number_confirm_for_lib(
+                this.item_cache[id].stream().distribution().holding_lib(),
+                value
+            );
+        } else {
+            return true;
+        }
+    };
+
     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);
+            if (!this._row_disabled(id)) {
+                if (this._confirm_row_field_application(id, key, value)) {
+                    this._row_field_value(id, key, value);
+                }
+            }
         }
     };
 
@@ -420,7 +553,7 @@ function BatchReceiver() {
                     } else {
                         alert(S("bib_lookup.not_found"));
                         if (is_actual_id) {
-                            self._init();
+                            self.init();
                         } else {
                             dojo.byId("bib_search_term").reset();
                             dojo.byId("bib_search_term").focus();
@@ -510,7 +643,7 @@ function BatchReceiver() {
             this.load_entry_form(this.issuances[0]);
         } else {
             alert(S("issuance_lookup.none"));
-            this._init();
+            this.init();
         }
 
     };
@@ -533,7 +666,7 @@ function BatchReceiver() {
         fieldmapper.standardRequest(
             ["open-ils.serial",
                 "open-ils.serial.items.receivable.by_issuance.atomic"], {
-                "params": [authtoken, this.issuance.id()],
+                "params": [this.authtoken, this.issuance.id()],
                 "async": true,
                 "onresponse": function(r) {
                     busy(false);
@@ -554,13 +687,17 @@ function BatchReceiver() {
                         } else {
                             alert(S("item_lookup.none"));
                             if (self.issuances.length) self.choose_issuance();
-                            else self._init();
+                            else self.init();
                         }
                     }
                 }
             }
         );
+    };
 
+    this.toggle_all_receive = function(checked) {
+        for (var id in this.rows)
+            this._disable_row(id, !checked);
     };
 
     this.build_batch_entry_row = function() {
@@ -585,14 +722,29 @@ function BatchReceiver() {
         node_by_name("circ_modifier", row).appendChild(
             this.batch_controls.circ_modifier =
                 this._extend_circ_modifier_for_batch(
-                    this._build_circ_modifier_dropdown()
+                    this._build_circ_modifier_dropdown() /* for all OUs */
                 )
         );
 
+        node_by_name("call_number", row).appendChild(
+            this.batch_controls.call_number = this._build_call_number_control()
+        );
+
         node_by_name("price", row).appendChild(
             this.batch_controls.price = dojo.create("textbox", {"size": 9})
         );
 
+        node_by_name("receive", row).appendChild(
+            dojo.create(
+                "checkbox", {
+                    "oncommand": function(ev) {
+                        self.toggle_all_receive(ev.target.checked);
+                    },
+                    "checked": "true"
+                }
+            )
+        );
+
         node_by_name("apply", row).appendChild(
             dojo.create("button", {
                 "label": S("apply"),
@@ -609,6 +761,9 @@ function BatchReceiver() {
             if (value != "" && value != -1)
                 this._set_all_enabled_rows(key, value);
         }
+
+        /* XXX genericize for all fields? */
+        delete this._has_confirmed_cn_for;
     };
 
     this.add_entry_row = function(item) {
@@ -643,6 +798,7 @@ function BatchReceiver() {
 
         n("note").appendChild(dojo.create("textbox", {"size": 20}));
         n("circ_modifier").appendChild(this._build_circ_modifier_dropdown());
+        n("call_number").appendChild(this._build_call_number_control(item));
         n("price").appendChild(dojo.create("textbox", {"size": 9}));
         n("receive").appendChild(this._build_receive_toggle(item));
 
@@ -651,14 +807,22 @@ function BatchReceiver() {
 
     this.receive = function() {
         var items = [];
+        var confirmed_missing_units = false;
+
         for (var id in this.rows) {
-            if (this._row_disabled(id)) 
+            if (this._row_disabled(id))
                 continue;
 
             var item = this.item_cache[id];
 
-            var barcode = this._row_field_value(id, "barcode");
-            if (barcode) {
+            /* Don't trim() call_number field, as existing call numbers
+             * are yielded by their label field, not by id, and if
+             * they start or end in spaces, we'll unintentionally create
+             * a new, different CN if we trim that */
+            var cn_string = this._row_field_value(id, "call_number");
+            var barcode = this._row_field_value(id, "barcode").trim();
+
+            if (barcode && cn_string.length) {
                 var unit = new sunit();
                 unit.barcode(barcode);
 
@@ -669,8 +833,17 @@ function BatchReceiver() {
                     }
                 );
 
-
+                unit.call_number(cn_string);
                 item.unit(unit);
+            } else if (barcode && !cn_string.length) {
+                alert(S("missing_cn"));
+                return;
+            } else if (!confirmed_missing_units) {
+                if (confirm(S("missing_units"))) {
+                    confirmed_missing_units = true;
+                } else {
+                    return;
+                }
             }
 
             var note_value = this._row_field_value(id, "note").trim();
@@ -690,7 +863,7 @@ function BatchReceiver() {
         busy(true);
         fieldmapper.standardRequest(
             ["open-ils.serial", "open-ils.serial.receive_items.one_unit_per"],{
-                "params": [authtoken, items],
+                "params": [this.authtoken, items, this.sub.record_entry()],
                 "async": true,
                 "oncomplete": function(r) {
                     try {
@@ -723,7 +896,9 @@ function BatchReceiver() {
                 try {
                     fieldmapper.standardRequest(
                         ["open-ils.cat", "open-ils.cat.item.barcode.autogen"], {
-                            "params": [authtoken, textbox.value, list.length],
+                            "params": [
+                                this.authtoken, textbox.value, list.length
+                            ],
                             "async": false,
                             "onresponse": function(r) {
                                 r = openils.Util.readResponse(r, false, true);
@@ -747,14 +922,15 @@ function BatchReceiver() {
         }
     };
 
-    this._init.apply(this, arguments);
+    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);
+    batch_receiver = new BatchReceiver(
+        (typeof ses == "function" ? ses() : 0) ||
+            cgi.param("ses") || dojo.cookie("ses"),
+        cgi.param("docid") || null
+    );
 }
index b1bf50a..46b158a 100644 (file)
@@ -96,6 +96,9 @@
                                     &staff.serial.batch_receive.circ_modifier;
                                 </h:th>
                                 <h:th>
+                                    &staff.serial.batch_receive.call_number;
+                                </h:th>
+                                <h:th>
                                     &staff.serial.batch_receive.note;
                                 </h:th>
                                 <h:th>
                                         label="&staff.serial.batch_receive.auto_generate;" />
                                 </h:td>
                                 <h:td name="circ_modifier" align="center"></h:td>
+                                <h:td name="call_number" align="center"></h:td>
                                 <h:td name="note"></h:td>
                                 <h:td name="location" align="center"></h:td>
                                 <h:td name="price"></h:td>
-                                <h:td name="receive"></h:td>
+                                <h:td name="receive" align="center"></h:td>
                                 <h:td name="apply"></h:td>
                             </h:tr>
                             <h:tr>
                                 <h:td name="holding_lib" align="center"></h:td>
                                 <h:td name="barcode"></h:td>
                                 <h:td name="circ_modifier" align="center"></h:td>
+                                <h:td name="call_number" align="center"></h:td>
                                 <h:td name="note"></h:td>
                                 <h:td name="location" align="center"></h:td>
                                 <h:td name="price"></h:td>
             </vbox>
         </vbox>
 
-        <hbox>
-            <button oncommand="batch_receiver._init();"
+        <hbox align="center">
+            <button oncommand="batch_receiver.init();"
                 label="&staff.serial.batch_receive.start_over;"
                 accesskey="&staff.serial.batch_receive.start_over.accesskey;" />
+            <!-- coming soon
+            <spacer flex="1" />
+            <checkbox oncommand="batch_receiver.toggle_receive_with_units(event);"
+                label="&staff.serial.batch_receive.with_units;"
+                accesskey="&staff.serial.batch_receive.with_units.accesskey;" />
+            -->
         </hbox>
     </box>
 </overlay>
index 95af456..c790a1a 100644 (file)
@@ -9,3 +9,4 @@ label.receiving { width: 20em; text-align: right; }
 .hideme { display: none; }
 #batch_receive_entry { padding-top: 10px; }
 #entry_submitter { padding: 20px 0; }
+menulist.cn { width: 12em; }