[open-ils-commits] r15600 - in trunk/Open-ILS: src/perlmods/OpenILS/Application/Acq web/js/dojo/openils/acq/nls web/js/ui/default/acq/common web/js/ui/default/acq/po web/templates/default/acq/common (senator)

svn at svn.open-ils.org svn at svn.open-ils.org
Fri Feb 19 15:00:39 EST 2010


Author: senator
Date: 2010-02-19 15:00:35 -0500 (Fri, 19 Feb 2010)
New Revision: 15600

Modified:
   trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm
   trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm
   trunk/Open-ILS/web/js/dojo/openils/acq/nls/acq.js
   trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js
   trunk/Open-ILS/web/js/ui/default/acq/po/view_po.js
   trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2
Log:
Acq: complete improvements of receive/unreceive for lineitems and copies

When viewing a purchase order, you can now recieve and un-receive individual
lineitems or even individual copies, and the PO viewing interface will
properly reflect all changes (to the states of the PO, the lineitems, and the
copies; to the amounts spent and encumbered; and to the state of widgets that
should appear or disappear based on PO/LI/copy state).


Modified: trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm	2010-02-19 17:08:37 UTC (rev 15599)
+++ trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm	2010-02-19 20:00:35 UTC (rev 15600)
@@ -1003,6 +1003,41 @@
     return undef;
 }
 
+sub build_price_summary {
+    my ($e, $po_id) = @_;
+
+    # fetch the fund debits for this purchase order
+    my $debits = $e->json_query({
+        "select" => {"acqfdeb" => [qw/encumbrance amount/]},
+        "from" => {
+            "acqlid" => {
+                "jub" => {
+                    "fkey" => "lineitem",
+                    "field" => "id",
+                    "join" => {
+                        "acqpo" => {
+                            "fkey" => "purchase_order", "field" => "id"
+                        }
+                    }
+                },
+                "acqfdeb" => {"fkey" => "fund_debit", "field" => "id"}
+            }
+        },
+        "where" => {"+acqpo" => {"id" => $po_id}}
+    });
+
+    my ($enc, $spent) = (0, 0);
+    for my $deb (@$debits) {
+        if($U->is_true($deb->{encumbrance})) {
+            $enc += $deb->{amount};
+        } else {
+            $spent += $deb->{amount};
+        }
+    }
+    ($enc, $spent);
+}
+
+
 sub retrieve_purchase_order_impl {
     my($e, $po_id, $options) = @_;
 
@@ -1038,31 +1073,7 @@
     }
 
     if($$options{flesh_price_summary}) {
-
-        # fetch the fund debits for this purchase order
-        my $debits = $e->json_query({
-            select => {acqfdeb => ["encumbrance", "amount"]},
-            from => {
-                acqlid => {
-                    jub => {fkey => "lineitem", field => "id", 
-                        join => {acqpo => {fkey => "purchase_order", field => "id"}}
-                    },
-                acqfdeb => {fkey => "fund_debit", field =>"id"}
-                }
-            },
-            where => {'+acqpo' => {id => $po_id}}
-        });
-
-        my $enc = 0;
-        my $spent = 0;
-        for my $deb (@$debits) {
-            if($U->is_true($deb->{encumbrance})) {
-                $enc += $deb->{amount};
-            } else {
-                $spent += $deb->{amount};
-            }
-        }
-
+        my ($enc, $spent) = build_price_summary($e, $po_id);
         $po->amount_encumbered($enc);
         $po->amount_spent($spent);
     }

Modified: trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm	2010-02-19 17:08:37 UTC (rev 15599)
+++ trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm	2010-02-19 20:00:35 UTC (rev 15600)
@@ -1,4 +1,5 @@
 package OpenILS::Application::Acq::BatchManager;
+use OpenILS::Application::Acq::Financials;
 use OpenSRF::AppSession;
 use OpenSRF::EX qw/:try/;
 use strict; use warnings;
@@ -328,6 +329,23 @@
 # been received, mark the lineitem as received
 # returns 1 on non-received, li on received, 0 on error
 # ----------------------------------------------------------------------------
+
+sub describe_affected_po {
+    my ($e, $po) = @_;
+
+    my ($enc, $spent) =
+        OpenILS::Application::Acq::Financials::build_price_summary(
+            $e, $po->id
+        );
+
+    +{$po->id => {
+            "state" => $po->state,
+            "amount_encumbered" => $enc,
+            "amount_spent" => $spent
+        }
+    };
+}
+
 sub check_lineitem_received {
     my($mgr, $li_id) = @_;
 
@@ -359,15 +377,13 @@
     $mgr->post_process( sub { create_lineitem_status_events($mgr, $li_id, 'aur.received'); });
 
     my $po;
-    my $result = {"li" => {$li->id => {"state" => $li->state}}};
     return 0 unless
         $skip_complete_check or (
             $po = check_purchase_order_received($mgr, $li->purchase_order)
         );
 
-    if (ref $po) {
-        $result->{"po"} = {$po->id => {"state" => $li->state}};
-    }
+    my $result = {"li" => {$li->id => {"state" => $li->state}}};
+    $result->{"po"} = describe_affected_po($mgr->editor, $po) if ref $po;
     return $result;
 }
 
@@ -497,9 +513,7 @@
     my $li = check_lineitem_received($mgr, $lid->lineitem) or return 0;
     return 1 if $li == 1; # li not received
 
-    my $po = check_purchase_order_received($mgr, $li->purchase_order) or return 0;
-    return $li if $po == 1;
-    return $po;
+    return check_purchase_order_received($mgr, $li->purchase_order) or return 0;
 }
 
 
@@ -844,9 +858,9 @@
             state => {'!=' => 'received'}
         }, {idlist=>1});
 
-    return 1 if @$non_recv_li;
+    my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
+    return $po if @$non_recv_li;
 
-    my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
     $po->state('received');
     return update_purchase_order($mgr, $po);
 }
@@ -1555,6 +1569,22 @@
 }
 
 
+# At the moment there's a lack of parallelism between the receive and unreceive
+# API methods for POs and the API methods for LIs and LIDs.  The methods for
+# POs stream back objects as they act, whereas the methods for LIs and LIDs
+# atomically return an object that describes only what changed (in LIs and LIDs
+# themselves or in the objects to which to LIs and LIDs belong).
+#
+# The methods for LIs and LIDs work the way they do to faciliate the UI's
+# maintaining correct information about the state of these things when a user
+# wants to receive or unreceive these objects without refreshing their whole
+# display.  The UI feature for receiving and un-receiving a whole PO just
+# refreshes the whole display, so this absence of parallelism in the UI is also
+# relected in this module.
+#
+# This could be neatened in the future by making POs receive and unreceive in
+# the same way the LIs and LIDs do.
+
 __PACKAGE__->register_method(
 	method => 'receive_lineitem_detail_api',
 	api_name	=> 'open-ils.acq.lineitem_detail.receive',
@@ -1564,7 +1594,10 @@
             {desc => 'Authentication token', type => 'string'},
             {desc => 'lineitem detail ID', type => 'number'}
         ],
-        return => {desc => '1 on success, Event on error'}
+        return => {desc =>
+            "on success, object describing changes to LID and possibly " .
+            "to LI and PO; on error, Event"
+        }
     }
 );
 
@@ -1575,34 +1608,29 @@
     return $e->die_event unless $e->checkauth;
     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
 
-    my $lid = $e->retrieve_acq_lineitem_detail([
-        $lid_id, {
-            flesh => 2,
-            flesh_fields => {
-                acqlid => ['lineitem'],
-                jub => ['purchase_order']
-            }
+    my $fleshing = {
+        "flesh" => 2, "flesh_fields" => {
+            "acqlid" => ["lineitem"], "jub" => ["purchase_order"]
         }
-    ]);
+    };
 
+    my $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
+
     return $e->die_event unless $e->allowed(
         'RECEIVE_PURCHASE_ORDER', $lid->lineitem->purchase_order->ordering_agency);
 
+    # update ...
     my $recvd = receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
 
-    # What's this business, you ask? We basically want to return a minimal
-    # set of information about what has changed as a result of the "receive
-    # lineitem detail" operation; remember: not only does the lineitem detail
-    # change state, but so might an LI and even a PO, and a good UI will want
-    # to reflect those changes.
-    $lid = $e->retrieve_acq_lineitem_detail(
-        [$lid_id, {"flesh" => 1, "flesh_fields" => {"acqlid" => ["lineitem"]}}]
-    );
+    # .. and re-retrieve
+    $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
+
+    # Now build result data structure.
     my $result = {"lid" => {$lid->id => {"recv_time" => $lid->recv_time}}};
 
     if (ref $recvd) {
         if ($recvd->class_name =~ /::purchase_order/) {
-            $result->{"po"} = {"id" => $recvd->id, "state" => $recvd->state};
+            $result->{"po"} = describe_affected_po($e, $recvd);
             $result->{"li"} = {
                 $lid->lineitem->id => {"state" => $lid->lineitem->state}
             };
@@ -1610,6 +1638,8 @@
             $result->{"li"} = {$recvd->id => {"state" => $recvd->state}};
         }
     }
+    $result->{"po"} ||=
+        describe_affected_po($e, $lid->lineitem->purchase_order);
 
     $e->commit;
     return $result;
@@ -1622,11 +1652,11 @@
         desc => 'Mark a lineitem as received',
         params => [
             {desc => 'Authentication token', type => 'string'},
-            {desc => 'lineitem detail ID', type => 'number'}
+            {desc => 'lineitem ID', type => 'number'}
         ],
         return => {desc =>
-            "on success, object containing an LI and possibly a PO; " .
-            "on error, event"
+            "on success, object describing changes to LI and possibly PO; " .
+            "on error, Event"
         }
     }
 );
@@ -1695,7 +1725,10 @@
             {desc => 'Authentication token', type => 'string'},
             {desc => 'lineitem detail ID', type => 'number'}
         ],
-        return => {desc => '1 on success, Event on error'}
+        return => {desc =>
+            "on success, object describing changes to LID and possibly " .
+            "to LI and PO; on error, Event"
+        }
     }
 );
 
@@ -1740,8 +1773,8 @@
     if ($po->state eq "received") {
         $po->state("on-order");
         $po = update_purchase_order($mgr, $po) or return $e->die_event;
-        $result->{"po"} = {$po->id => {"state" => $po->state}};
     }
+    $result->{"po"} = describe_affected_po($e, $po);
 
     $e->commit and return $result or return $e->die_event;
 }
@@ -1750,12 +1783,15 @@
 	method => 'rollback_receive_lineitem_api',
 	api_name	=> 'open-ils.acq.lineitem.receive.rollback',
 	signature => {
-        desc => 'Mark a lineitem as received',
+        desc => 'Mark a lineitem as Un-received',
         params => [
             {desc => 'Authentication token', type => 'string'},
-            {desc => 'lineitem detail ID', type => 'number'}
+            {desc => 'lineitem ID', type => 'number'}
         ],
-        return => {desc => 'altered objects on success, event on error'}
+        return => {desc =>
+            "on success, object describing changes to LI and possibly PO; " .
+            "on error, Event"
+        }
     }
 );
 
@@ -1781,8 +1817,8 @@
     if ($po->state eq "received") {
         $po->state("on-order");
         $po = update_purchase_order($mgr, $po) or return $e->die_event;
-        $result->{"po"} = {$po->id => {"state" => $po->state}};
     }
+    $result->{"po"} = describe_affected_po($e, $po);
 
     $e->commit and return $result or return $e->die_event;
 }

Modified: trunk/Open-ILS/web/js/dojo/openils/acq/nls/acq.js
===================================================================
--- trunk/Open-ILS/web/js/dojo/openils/acq/nls/acq.js	2010-02-19 17:08:37 UTC (rev 15599)
+++ trunk/Open-ILS/web/js/dojo/openils/acq/nls/acq.js	2010-02-19 20:00:35 UTC (rev 15600)
@@ -18,5 +18,7 @@
     'EXPORT_SAVE_DIALOG_TITLE': "Save field values to a file",
     'EXPORT_SHORT_LIST': "Not all of the selected items had the attribute '${0}'.\nChoose OK to save those values that could be found.",
     'EXPORT_EMPTY_LIST': "No values for attribute '${0}' found.",
-    'UNRECEIVE_LI': "Are you sure you want to mark this lineitem as UN-received?"
+    'UNRECEIVE_LI': "Are you sure you want to mark this lineitem as UN-received?",
+
+    'UNRECEIVE_LID': "Are you sure you want to mark this copy as UN-received?",
 }

Modified: trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js
===================================================================
--- trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js	2010-02-19 17:08:37 UTC (rev 15599)
+++ trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js	2010-02-19 20:00:35 UTC (rev 15600)
@@ -251,7 +251,7 @@
         priceInput.onchange = function() { self.updateLiPrice(priceInput, li) };
 
         // show either "mark received" or "unreceive" as appropriate
-        this.updateReceivedness(li, row);
+        this.updateLiReceivedness(li, row);
 
         if (!skip_final_placement) {
             self.tbody.appendChild(row);
@@ -261,7 +261,7 @@
         }
     };
 
-    this.updateReceivedness = function(li, row) {
+    this.updateLiReceivedness = function(li, row) {
         if (typeof(row) == "undefined")
             row = dojo.query('tr[li="' + li.id() + '"]', "acq-lit-tbody")[0];
 
@@ -281,18 +281,15 @@
                     openils.Util.hide(real_copies_link);
                     openils.Util.hide(unrecv_link);
                     openils.Util.show(recv_link, "inline");
-                    if (typeof(recv_link.onclick) != "function")
-                        recv_link.onclick = function() { self.receiveLi(li); };
+                    recv_link.onclick = function() { self.issueReceive(li); };
                     return;
                 case "received":
                     openils.Util.hide(recv_link);
                     openils.Util.show(unrecv_link, "inline");
-                    if (typeof(unrecv_link.onclick) != "function") {
-                        unrecv_link.onclick = function() {
-                            if (confirm(localeStrings.UNRECEIVE_LI))
-                                self.unReceiveLi(li);
-                        };
-                    }
+                    unrecv_link.onclick = function() {
+                        if (confirm(localeStrings.UNRECEIVE_LI))
+                            self.issueReceive(li, /* rollback */ true);
+                    };
                     // TODO we should allow editing before receipt, in which case the
                     // test should be "if 1 or more real (acp) copies exist
                     openils.Util.show(real_copies_link);
@@ -902,23 +899,50 @@
             }
         );
 
-        var recv_link = dojo.query('[name=receive]', row)[0];
-        if(copy.recv_time()) {
-            openils.Util.hide(recv_link);
-        } else {
-            recv_link.onclick = function() {
-                self.receiveLid(copy);
+        this.updateLidReceivedness(copy, row);
+    };
+
+    this.updateLidReceivedness = function(copy, row) {
+        if (typeof(row) == "undefined") {
+            row = dojo.query(
+                'tr[copy_id="' + copy.id() + '"]', this.copyTbody
+            )[0];
+        }
+
+        var self = this;
+        var recv_link = nodeByName("receive", row);
+        var unrecv_link = nodeByName("unreceive", row);
+        var del_link = nodeByName("delete", row);
+
+        if (this.isPO) {
+            openils.Util.hide(del_link.parentNode);
+
+            /* Avoid showing (un)receive links for virtual copies */
+            if (copy.id() > 0) {
+                if(copy.recv_time()) {
+                    openils.Util.hide(recv_link);
+                    openils.Util.show(unrecv_link);
+                    unrecv_link.onclick = function() {
+                        if (confirm(localeStrings.UNRECEIVE_LID))
+                            self.issueReceive(copy, /* rollback */ true);
+                    };
+                } else {
+                    openils.Util.hide(unrecv_link);
+                    openils.Util.show(recv_link);
+                    recv_link.onclick = function() { self.issueReceive(copy); };
+                }
+            } else {
+                openils.Util.hide(unrecv_link);
                 openils.Util.hide(recv_link);
             }
-        }
+        } else {
+            openils.Util.hide(unrecv_link);
+            openils.Util.hide(recv_link);
 
-        if(this.isPO) {
-            openils.Util.hide(dojo.query('[name=delete]', row)[0].parentNode);
-        } else {
-            dojo.query('[name=delete]', row)[0].onclick = 
-                function() { self.deleteCopy(row) };
+            del_link.onclick = function() { self.deleteCopy(row) };
+            openils.Util.show(del_link.parentNode);
         }
-    };
+    }
 
     this.deleteCopy = function(row) {
         var copy = this.copyCache[row.getAttribute('copy_id')];
@@ -1175,79 +1199,55 @@
         );
     }
 
-    this.receiveLi = function(li) {
-        /* (For now) there shall be no marking LIs received except from the
-         * actual "view PO" interface. */
+    this.issueReceive = function(obj, rollback) {
+        /* (For now) there shall be no marking LI or LIDs (un)received
+         * except from the actual "view PO" interface. */
         if (!this.isPO) return;
 
-        var self = this;
+        var part =
+            {"jub": "lineitem", "acqlid": "lineitem_detail"}[obj.classname];
+        var method =
+            "open-ils.acq." + part + ".receive" + (rollback ? ".rollback" : "");
+
         progressDialog.show(true);
         fieldmapper.standardRequest(
-            ["open-ils.acq", "open-ils.acq.lineitem.receive"], {
+            ["open-ils.acq", method], {
                 "async": true,
-                "params": [this.authtoken, li.id()],
+                "params": [this.authtoken, obj.id()],
                 "onresponse": function(r) {
-                    self.handleReceiveOrRollback(openils.Util.readResponse(r));
+                    self.handleReceive(openils.Util.readResponse(r));
                 },
-                "oncomplete": function() {
-                    progressDialog.hide();
-                }
+                "oncomplete": function() { progressDialog.hide(); }
             }
         );
-    }
+    };
 
-    this.handleReceiveOrRollback = function(resp) {
+    /**
+     * Handles the responses from receive and rollback ML calls.
+     */
+    this.handleReceive = function(resp) {
         if (resp) {
             if (resp.li) {
                 for (var li_id in resp.li) {
                     for (var key in resp.li[li_id])
                         self.liCache[li_id][key](resp.li[li_id][key]);
-                    self.updateReceivedness(self.liCache[li_id]);
+                    self.updateLiReceivedness(self.liCache[li_id]);
                 }
             }
             if (resp.po) {
                 if (typeof(self.poUpdateCallback) == "function")
                     self.poUpdateCallback(resp.po);
             }
-        }
-    }
-
-    this.unReceiveLi = function(li) {
-        /* (For now) there shall be no marking LIs un-received except from the
-         * actual "view PO" interface. */
-        if (!this.isPO) return;
-
-        var self = this;
-        progressDialog.show(true);
-        fieldmapper.standardRequest(
-            ["open-ils.acq", "open-ils.acq.lineitem.receive.rollback"], {
-                "async": true,
-                "params": [this.authtoken, li.id()],
-                "onresponse": function(r) {
-                    self.handleReceiveOrRollback(openils.Util.readResponse(r));
-                },
-                "oncomplete": function() {
-                    progressDialog.hide();
+            if (resp.lid) {
+                for (var lid_id in resp.lid) {
+                    for (var key in resp.lid[lid_id])
+                        self.copyCache[lid_id][key](resp.lid[lid_id][key]);
+                    self.updateLidReceivedness(self.copyCache[lid_id]);
                 }
             }
-        );
-    }
+        }
+    };
 
-    this.receiveLid = function(li) {
-        if(!this.isPO) return;
-        progressDialog.show(true);
-        fieldmapper.standardRequest(
-            ['open-ils.acq', 'open-ils.acq.lineitem_detail.receive'],
-            {   async: true,
-                params: [this.authtoken, li.id()],
-                onresponse : function(r) {
-                    var resp = openils.Util.readResponse(r);
-                    progressDialog.hide();
-                },
-            }
-        );
-    }
-
     this.rollbackPoReceive = function() {
         if(!this.isPO) return;
         if(!confirm(localeStrings.ROLLBACK_PO_RECEIVE_CONFIRM)) return;

Modified: trunk/Open-ILS/web/js/ui/default/acq/po/view_po.js
===================================================================
--- trunk/Open-ILS/web/js/ui/default/acq/po/view_po.js	2010-02-19 17:08:37 UTC (rev 15599)
+++ trunk/Open-ILS/web/js/ui/default/acq/po/view_po.js	2010-02-19 20:00:35 UTC (rev 15600)
@@ -8,12 +8,28 @@
 
 function updatePoState(po_info) {
     var data = po_info[PO.id()];
-    if (data && data.state) {
-        PO.state(data.state);
-        dojo.byId("acq-po-view-state").innerHTML = PO.state(); // TODO i18n
+    if (data) {
+        for (var key in data)
+            PO[key](data[key]);
+        renderPo();
     }
 }
 
+function renderPo() {
+    dojo.byId("acq-po-view-id").innerHTML = PO.id();
+    dojo.byId("acq-po-view-name").innerHTML = PO.name();
+    dojo.byId("acq-po-view-total-li").innerHTML = PO.lineitem_count();
+    dojo.byId("acq-po-view-total-enc").innerHTML = PO.amount_encumbered();
+    dojo.byId("acq-po-view-total-spent").innerHTML = PO.amount_spent();
+    dojo.byId("acq-po-view-state").innerHTML = PO.state(); // TODO i18n
+
+    if(PO.state() == "pending") {
+        openils.Util.show("acq-po-activate");
+        if (PO.lineitem_count() > 1)
+            openils.Util.show("acq-po-split");
+    }
+}
+
 function init() {
     liTable = new AcqLiTable();
     liTable.reset();
@@ -25,20 +41,8 @@
         {   async: true,
             params: [openils.User.authtoken, poId, {flesh_price_summary:true, flesh_lineitem_count:true}],
             oncomplete: function(r) {
-                PO = openils.Util.readResponse(r);
-                dojo.byId('acq-po-view-id').innerHTML = PO.id();
-                dojo.byId('acq-po-view-name').innerHTML = PO.name();
-                dojo.byId('acq-po-view-total-li').innerHTML = PO.lineitem_count();
-                dojo.byId('acq-po-view-total-enc').innerHTML = PO.amount_encumbered();
-                dojo.byId('acq-po-view-total-spent').innerHTML = PO.amount_spent();
-                dojo.byId('acq-po-view-state').innerHTML = PO.state(); // TODO i18n
-
-                if(PO.state() == 'pending') {
-                    openils.Util.show('acq-po-activate');
-                    if (PO.lineitem_count() > 1) {
-                        openils.Util.show('acq-po-split');
-                    }
-                }
+                PO = openils.Util.readResponse(r); /* save PO globally */
+                renderPo();
             }
         }
     );

Modified: trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2
===================================================================
--- trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2	2010-02-19 17:08:37 UTC (rev 15599)
+++ trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2	2010-02-19 20:00:35 UTC (rev 15600)
@@ -92,7 +92,7 @@
                         </table>
                     </td>
                     <td><a class='hidden' name='real_copies_link' href='javascript:void(0);'>Update&nbsp;Barcodes</a></td>
-                    <td><a name='receive_link' href='javascript:void(0);'>Mark&nbsp;Received</a><a name='unreceive_link' href='javascript:void(0);'>Unreceive</a></td>
+                    <td><a name='receive_link' href='javascript:void(0);'>Mark&nbsp;Received</a><a name='unreceive_link' href='javascript:void(0);'>Un-Receive</a></td>
                     </td>
                     <td><a name='copieslink' href='javascript:void(0);'>Copies(<span name='count'>0</span>)</a></td>
                     <td>
@@ -249,7 +249,7 @@
                     <td><div name='cn_label'/></td>
                     <td><div name='barcode'/></td>
                     <td><div name='note'/></td>
-                    <td><a href='javascript:void(0);' name='receive'>Mark&nbsp;Received</a></td>
+                    <td><a href='javascript:void(0);' name='receive'>Mark&nbsp;Received</a><a href='javascript:void(0);' name='unreceive'>Un-Receive</a></td>
                     <td><div name='delete' dojoType='dijit.form.Button' style='color:red;'>X</div></td>
                 </tr>
             </tbody>



More information about the open-ils-commits mailing list