It's a lineitem detail (copy) batch receiver
authorLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Wed, 3 Aug 2011 20:05:21 +0000 (16:05 -0400)
committerMike Rylander <mrylander@gmail.com>
Mon, 19 Sep 2011 15:29:58 +0000 (11:29 -0400)
You can get to it via a button in the upper right-hand corner of
the view-invoice interface, assuming you're looking at an invoice that's
already been created/saved (and which has acqlid objects in a receivable
state), not one you're just now creating.

Signed-off-by: Lebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/templates/acq/invoice/receive.tt2 [new file with mode: 0644]
Open-ILS/src/templates/acq/invoice/view.tt2
Open-ILS/web/js/dojo/openils/acq/nls/acq.js
Open-ILS/web/js/ui/default/acq/invoice/receive.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/acq/invoice/view.js

diff --git a/Open-ILS/src/templates/acq/invoice/receive.tt2 b/Open-ILS/src/templates/acq/invoice/receive.tt2
new file mode 100644 (file)
index 0000000..6a2abb9
--- /dev/null
@@ -0,0 +1,64 @@
+[% WRAPPER "base.tt2" %]
+[% ctx.page_title = "Acquisitions Invoice Receiving" %]
+<script type="text/javascript">var inv_id = "[% ctx.page_args.0 %]";</script>
+<script type="text/javascript"
+    src="[% ctx.media_prefix %]/js/ui/default/acq/invoice/receive.js"></script>
+<style type="text/css">
+    #copy_table thead tr th {
+        font-weight: bold;
+        font-size: 110%;
+        text-align: left;
+        background-color: #e2e2e2;
+    }
+    #copy_table tbody { margin: 1ex; }
+    #copy_table tbody tr { background-color: #aaa; }
+    #copy_table tbody tr.copy-row { background-color: #ccc; }
+    #copy_table tbody tr.copy-row:nth-child(odd) { background-color: #ddd; }
+    #copy_table tbody tr.copy-row td { padding: 2px; }
+    .receive-button { margin: 1ex 0; }
+    .spinner-cell { padding: 1ex; background-color: #ddd; }
+</style>
+<div>
+    <p><big>[% ctx.page_title %]</big></p>
+    <p><strong id="inv-header"></strong></p>
+    <div id="non-empty">
+        <p>
+            <span id="set-list-mode">[ <a id="set-list-mode-link" href="javascript:void(0);">Use List Mode</a> ]</span>
+            <span id="set-number-mode">[ <a id="set-number-mode-link" href="javascript:void(0);">Use Numeric Mode</a> ]</span>
+        </p>
+        <div class="receive-button">
+            <button onclick="copy_table.receive_selected();">Receive Selected
+                Copies</button>
+        </div>
+        <table id="copy_table" width="100%">
+            <thead id="list-mode-headings">
+                <tr>
+                    <th>
+                        <input type="checkbox" checked="checked" id="select_all"
+                            /><label for="select_all"> Select All</label>
+                    </th>
+                    <th>Owning Branch</th>
+                    <th>Shelving Location</th>
+                    <th>Collection Code</th>
+                    <th>Fund</th>
+                    <th>Circ Modifier</th>
+                    <th>Callnumber</th>
+                    <th>Barcode</th>
+                </tr>
+            </thead>
+            <tbody id="rows-here">
+            </tbody>
+        </table>
+        <div class="receive-button">
+            <button onclick="copy_table.receive_selected();">Receive Selected
+                Copies</button>
+        </div>
+    </div>
+    <div class="hidden" id="empty">
+        This invoice has no more copies to receive.
+    </div>
+    <div class="hidden">
+        <div dojoType="openils.widget.ProgressDialog" jsId="progress_dialog"></div>
+    </div>
+</div>
+[% END %]
index 0ddca25..a6289e3 100644 (file)
@@ -5,7 +5,7 @@
 
     <div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
         <div> Invoice </div>
-        <div</div>
+        <div id="acq-view-invoice-receive" class="hidden"><button id="acq-view-invoice-receive-link">Receive Items</button></div>
     </div>
 
     <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
index 0525d48..395f79e 100644 (file)
@@ -80,6 +80,7 @@
     "LIBRARY_INITIATED": "Library Initiated",
     "DEL_LI_FROM_PO": "That item has already been ordered!  Deleting it now will not revoke or modify any order that has been placed with a vendor.  Deleting the item may put the system's idea of your purchase order in a state that is inconsistent with reality.  Are you sure you mean to do this?",
     "ADD_LI_TO_PO_BAD_PO_STATE" : "The selected PO has already been activated",
-    "ADD_LI_TO_PO_BAD_LI_STATE" : "The selected lineitem is not in a state that can be added to a purchase order"
-
+    "ADD_LI_TO_PO_BAD_LI_STATE" : "The selected lineitem is not in a state that can be added to a purchase order",
+    "INVOICE_NUMBER": "Invoice #${0}",
+    "COPIES_TO_RECEIVE": "Number of copies to receive: "
 }
diff --git a/Open-ILS/web/js/ui/default/acq/invoice/receive.js b/Open-ILS/web/js/ui/default/acq/invoice/receive.js
new file mode 100644 (file)
index 0000000..3930b52
--- /dev/null
@@ -0,0 +1,356 @@
+dojo.require("dijit.form.Button");
+dojo.require("dijit.form.NumberSpinner");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.acq.Lineitem");
+dojo.require("openils.widget.AutoFieldWidget");
+dojo.require("openils.widget.ProgressDialog");
+dojo.requireLocalization("openils.acq", "acq");
+
+var copy_table;
+var localeStrings = dojo.i18n.getLocalization("openils.acq", "acq");
+
+function ReceivableCopyTable() {
+    var self = this;
+
+    this._init = function() {
+        this.columns = ["owning_lib", "location", "collection_code",
+            "circ_modifier", "fund", "cn_label", "barcode"];
+
+        this.tbody = dojo.byId("rows-here");
+        this.pcrud = new openils.PermaCrud();
+
+        this.mode = "number";   /* can be "number" or "list" */
+        this.some_receiving_done = false;
+
+        this._init_select_all();
+    };
+
+    this._init_select_all = function() {
+        dojo.byId("select_all").onchange = function() {
+            var checked = this.checked;
+            dojo.query("input[type='checkbox']", self.tbody).forEach(
+                function(cb) { cb.checked = checked; }
+            );
+        };
+    };
+
+    this._set_invoice_header = function() {
+        dojo.byId("inv-header").innerHTML = dojo.string.substitute(
+            localeStrings.INVOICE_NUMBER, [this.invoice.inv_ident()]
+        );
+    };
+
+    this._configure_for_mode = function() {
+        if (this.mode == "list") {
+            openils.Util.show("list-mode-headings", "table-header-group");
+            openils.Util.hide("set-list-mode");
+            openils.Util.show("set-number-mode");
+            dojo.byId("set-number-mode-link").onclick = function() {
+                self.reset("number");
+                self.load();
+            };
+        } else { /* number */
+            openils.Util.hide("list-mode-headings");
+            openils.Util.show("set-list-mode");
+            openils.Util.hide("set-number-mode");
+            dojo.byId("set-list-mode-link").onclick = function() {
+                self.reset("list");
+                self.load();
+            };
+        }
+    };
+
+    this._get_receivable_details = function(li) {
+        return li.lineitem_details().filter(
+            function(lid) { return (!lid.recv_time() && !lid.cancel_reason()); }
+        );
+    };
+
+    this._create_receiver = function(lid, tr, precheck) {
+        var args = {
+            "type": "checkbox",
+            "name": "receive",
+            "value": lid.id()
+        };
+
+        if (precheck) args.checked = "checked";
+
+        dojo.create("input", args, dojo.create("td", null, tr));
+    };
+
+    this._get_selected_list_mode = function() {
+        return dojo.query("input[type=checkbox]", this.tbody).filter(
+            function(cb) { return cb.checked; }
+        ).map(
+            function(cb) { return cb.value; }
+        );
+    };
+
+    this._get_selected_number_mode = function() {
+        var list = [];
+        for (var li_id in this.spinners) {
+            var spinner = this.spinners[li_id];
+            var li = spinner._li;
+
+            var number = spinner.attr("value");
+            list = list.concat(
+                this._get_receivable_details(li).slice(0, number)
+            );
+        }
+        return list.map(function(lid) { return lid.id(); });
+    };
+
+    /* The first time this interface is loaded, use the phys_item_count field
+     * (the "# paid" column on an invoice) to determing how man items to
+     * preselect.  Otherwise use 0.
+     */
+    this._number_to_preselect = function(ie, li) {
+        return (this.some_receiving_done) ? 0 :
+            Number(ie.phys_item_count() || 0);
+
+//        var n = Number(ie.phys_item_count() || 0) -
+//            li.lineitem_details().filter(
+//                function(lid) {
+//                    return lid.recv_time() || lid.cancel_reason()
+//                }
+//            ).length;
+//
+//        return n > 0 ? n : 0;
+    };
+
+    this._add_lineitem_number_mode = function(details, li, preselect_count) {
+        var tr = dojo.create("tr", null, this.tbody);
+        var td = dojo.create("td", {
+            "colspan": 1 + this.columns.length,
+            "className": "spinner-cell"
+        }, tr);
+
+        var span_id = "number-mode-li-" + li.id();
+
+        td.innerHTML = localeStrings.COPIES_TO_RECEIVE;
+        dojo.create("span", {"id": span_id}, td);
+
+        var max = details.length;
+        var value = (preselect_count <= max ? preselect_count : max);
+
+        this.spinners[li.id()] = new dijit.form.NumberSpinner({
+            "constraints": {"min": 0, "max": max},
+            "value": value
+        }, span_id);
+        this.spinners[li.id()]._li = li;
+    };
+
+    this._add_lineitem_list_mode = function(details, li, preselect_count) {
+        details.forEach(
+            function(lid) {
+                dump("preselect_count "+ preselect_count+"\n");
+                self.add_lineitem_detail(
+                    lid, li, Boolean(preselect_count-- > 0)
+                );
+            }
+        );
+    };
+
+    this.add_lineitem_detail = function(lid, li, precheck) {
+        var tr = dojo.create(
+            "tr", {"className": "copy-row"}, this.tbody
+        );
+
+        /* Make receive checkbox cell. */
+        this._create_receiver(lid, tr, precheck);
+
+        /* Make cells for all the other columns.  Using a read-only
+         * AutoFieldWidget to show the value of each field on a lineitem
+         * detail is much easier than worrying about fleshing enough
+         * information to do the same ourselves. */
+        this.columns.forEach(
+            function(column) {
+                var td = dojo.create("td", null, tr);
+                new openils.widget.AutoFieldWidget({
+                    "parentNode": dojo.create("div", null, td),
+                    "fmField": column,
+                    "fmObject": lid,
+                    "readOnly": true,
+                    "dijitArgs": {"labelType": (column=='fund') ? "html" : null}
+                }).build();
+            }
+        );
+    };
+
+    /* /maybe/ add a lineitem to the table, if it has any lineitem details
+     * that are still receivable, and preselect lineitem details up to the
+     * number specified in ie.phys_item_count() */
+    this.add_lineitem = function(ie, li, displayHTML) {
+        var receivable_details = this._get_receivable_details(li);
+        if (!receivable_details.length) return;
+
+        /* show lineitem overall description */
+        /* add rows for copies (lineitem details) */
+        dojo.create(
+            "td", {
+                "colspan": 1 + this.columns.length,
+                "innerHTML": displayHTML
+            }, dojo.create("tr", null, this.tbody)
+        );
+
+        /* build look-up table */
+        receivable_details.forEach(
+            function(lid) { self.li_by_lid[lid.id()] = li; }
+        );
+
+        /* Render something for receiving the lineitem details, depending
+         * on mode. */
+        this["_add_lineitem_" + this.mode + "_mode"](
+            receivable_details, li, this._number_to_preselect(ie, li)
+        );
+    };
+
+    this.reset = function(mode) {
+        if (mode)
+            this.mode = mode;
+
+        this.user_has_acked = [];
+        this.li_by_lid = {};
+
+        if (this.spinners) {
+            for (var key in this.spinners)
+                this.spinners[key].destroy();
+        }
+
+        this.spinners = {};
+
+        this._configure_for_mode();
+
+        dojo.empty(this.tbody);
+    };
+
+    /* It's important to remember that an invoice doesn't actually have
+     * lineitems, but rather is made up of invoice entries and invoice items.
+     * Invoice entries usually link to lineitems, though (invoice items
+     * usually link to po_items).
+     */
+    this.load = function(inv_id) {
+        if (inv_id)
+            this.inv_id = inv_id;
+
+        this.reset();
+        progress_dialog.show(true);
+
+        if (!this.invoice) {
+            this.invoice = this.pcrud.retrieve("acqinv", this.inv_id);
+            this._set_invoice_header();
+        }
+
+        this.pcrud.search("acqie", {"invoice": this.inv_id}).forEach(
+            function(entry) {
+                if (entry.lineitem()) {
+                    openils.acq.Lineitem.fetchAndRender(
+                        entry.lineitem(),
+                        {"flesh_li_details": true, "flesh_notes": true},
+                        function(li, str) { self.add_lineitem(entry, li, str); }
+                    );
+                }
+            }
+        );
+
+        if (openils.Util.objectProperties(this.li_by_lid).length) {
+            openils.Util.show("non-empty");
+            openils.Util.hide("empty");
+        } else {
+            openils.Util.hide("non-empty");
+            openils.Util.show("empty");
+        }
+        progress_dialog.hide();
+    };
+
+    /* returns an array of lineitem_detail IDs */
+    this.get_selected = function() {
+        return this["_get_selected_" + this.mode + "_mode"]();
+    };
+
+    this.receive_lineitem_detail = function(id_list, index) {
+        if (index >= id_list.length) {
+            progress_dialog.hide();
+            this.load();
+
+            return;
+        }
+
+        var lid_id = id_list[index];
+        var li = this.li_by_lid[lid_id];
+
+        if (!this.check_lineitem_alerts(li)) {
+            self.receive_lineitem_detail(id_list, ++index);
+            return;
+        }
+
+        fieldmapper.standardRequest(
+            ["open-ils.acq", "open-ils.acq.lineitem_detail.receive"], {
+                "async": false,
+                "params": [openils.User.authtoken, lid_id],
+                "oncomplete": function(r) {
+                    if (r = openils.Util.readResponse(r)) {
+                        self.some_receiving_done = true;
+                        /* receive the next lid in our list */
+                        self.receive_lineitem_detail(id_list, ++index);
+                    }
+                }
+            }
+        );
+    };
+
+    this.receive_selected = function() {
+        var lid_ids = this.get_selected();
+
+        progress_dialog.show(true);
+
+        this.receive_lineitem_detail(lid_ids, 0);
+    };
+
+    /* 1st of 2 functions all but copied from li_table.js. Refactor this and
+     * that to share code from a 3rd place.
+     */
+    this.check_lineitem_alerts = function(lineitem) {
+        var alert_notes = lineitem.lineitem_notes().filter(
+            function(o) { return Boolean(o.alert_text()); }
+        );
+
+        for (var i = 0; i < alert_notes.length; i++) {
+            if (this.user_has_acked[alert_notes[i].id()])
+                continue;
+            else if (!this.confirm_alert(li, alert_notes[i]))
+                return false;
+            else
+                this.user_has_acked[alert_notes[i].id()] = true;
+        }
+
+        return true;
+    };
+
+    /* 2nd of 2 functions all but copied from li_table.js. Refactor this and
+     * that to share code from a 3rd place.
+     */
+    this.confirm_alert = function(lineitem, note) {
+        return confirm(
+            dojo.string.substitute(
+                localeStrings.CONFIRM_LI_ALERT, [
+                    (new openils.acq.Lineitem({"lineitem": lineitem})).findAttr(
+                        "title", "lineitem_marc_attr_definition"
+                    ),
+                    note.alert_text().code(),
+                    note.alert_text().description() || "",
+                    note.value()
+                ]
+            )
+        );
+    };
+
+    this._init.apply(this, arguments);
+}
+
+function my_init() {
+    copy_table = new ReceivableCopyTable();
+    copy_table.load(inv_id);
+}
+
+openils.Util.addOnLoad(my_init);
index e7068b1..a326299 100644 (file)
@@ -414,6 +414,22 @@ function addInvoiceItem(item) {
     updateTotalCost();
 }
 
+function updateReceiveLink(li) {
+    if (!invoiceId)
+        return; /* can't do this with unsaved invoices */
+
+    var link = dojo.byId("acq-view-invoice-receive-link");
+    if (link.onclick) return; /* only need to do this once */
+
+    /* don't do this if there's nothing receivable on the lineitem */
+    if (li.order_summary().recv_count() + li.order_summary().cancel_count() >=
+        li.order_summary().item_count())
+        return;
+
+    openils.Util.show("acq-view-invoice-receive");
+    link.onclick = function() { location.href =  oilsBasePath + '/acq/invoice/receive/' + invoiceId; };
+}
+
 function addInvoiceEntry(entry) {
 
     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
@@ -439,6 +455,8 @@ function addInvoiceEntry(entry) {
             entry.purchase_order(li.purchase_order());
             nodeByName('title_details', row).innerHTML = html;
 
+            updateReceiveLink(li);
+
             dojo.forEach(
                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
                 function(field) {