[open-ils-commits] r15844 - in trunk/Open-ILS: examples src/extras src/perlmods/OpenILS/Application/Acq web/js/dojo/openils/acq/nls web/js/ui/default/acq/common web/js/ui/default/acq/picklist web/js/ui/default/acq/po web/opac/locale/en-US web/templates/default/acq/common web/templates/default/acq/po xul/staff_client/chrome/content/main (senator)

svn at svn.open-ils.org svn at svn.open-ils.org
Mon Mar 15 04:52:35 EDT 2010


Author: senator
Date: 2010-03-15 04:52:29 -0400 (Mon, 15 Mar 2010)
New Revision: 15844

Modified:
   trunk/Open-ILS/examples/fm_IDL.xml
   trunk/Open-ILS/src/extras/ils_events.xml
   trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm
   trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.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/picklist/view.js
   trunk/Open-ILS/web/js/ui/default/acq/po/view_po.js
   trunk/Open-ILS/web/opac/locale/en-US/lang.dtd
   trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2
   trunk/Open-ILS/web/templates/default/acq/po/view.tt2
   trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js
   trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
Log:
Acq: cancel POs, lineitems, or individual copies from the PO interface

Still needs some adjustments: e.g., you can still "receive" lineitems even
when all the individual copies are canceled, and things like that.


Modified: trunk/Open-ILS/examples/fm_IDL.xml
===================================================================
--- trunk/Open-ILS/examples/fm_IDL.xml	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/examples/fm_IDL.xml	2010-03-15 08:52:29 UTC (rev 15844)
@@ -5406,7 +5406,7 @@
 			<link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
 			<link field="location" reltype="has_a" key="id" map="" class="acpl"/>
 			<link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
-			<link field="cancel_reason" reltype="might_have" key="id" map="" class="acqcr"/>
+			<link field="cancel_reason" reltype="has_a" key="id" map="" class="acqcr"/>
 		</links>
 	</class>
 

Modified: trunk/Open-ILS/src/extras/ils_events.xml
===================================================================
--- trunk/Open-ILS/src/extras/ils_events.xml	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/src/extras/ils_events.xml	2010-03-15 08:52:29 UTC (rev 15844)
@@ -927,9 +927,13 @@
 		<desc xml:lang="en-US">The lineitem has no price</desc>
 	</event>
 
+	<event code='10101' textcode='ACQ_ALREADY_CANCELED'>
+		<desc xml:lang="en-US">The object is already canceled.</desc>
+	</event>
+	<event code='10102' textcode='ACQ_NOT_CANCELABLE'>
+		<desc xml:lang="en-US">The object is not in a cancelable state.</desc>
+	</event>
 
-
-
 	<!-- ================================================================ -->
 
 </ils_events>

Modified: trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm	2010-03-15 08:52:29 UTC (rev 15844)
@@ -893,12 +893,17 @@
 sub retrieve_purchase_order_impl {
     my($e, $po_id, $options) = @_;
 
+    # let's just always flesh this if it's there. what the hey.
+    my $flesh = {
+        "flesh" => 1, "flesh_fields" => {"acqpo" => ["cancel_reason"]}
+    };
+
     $options ||= {};
-    my $po = $e->retrieve_acq_purchase_order(
-        $options->{"flesh_notes"} ? [
-            $po_id, {"flesh" => 1, "flesh_fields" => {"acqpo" => ["notes"]}}
-        ] : $po_id
-    ) or return $e->event;
+    if ($options->{"flesh_notes"}) {
+        push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
+    }
+    my $po = $e->retrieve_acq_purchase_order([$po_id, $flesh])
+        or return $e->event;
 
     if($$options{flesh_lineitems}) {
 

Modified: trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm	2010-03-15 08:52:29 UTC (rev 15844)
@@ -101,6 +101,9 @@
             push(@{$flesh->{flesh_fields}->{jub}}, 'lineitem_notes');
             $flesh->{flesh_fields}->{acqlin} = ['alert_text'];
         }
+        if($$options{"flesh_cancel_reason"}) {
+            push @{$flesh->{flesh_fields}->{jub}}, "cancel_reason";
+        }
         push(@{$flesh->{flesh_fields}->{jub}}, 'attributes') if $$options{flesh_attrs};
     }
 
@@ -109,7 +112,7 @@
     if($$options{flesh_li_details}) {
         my $ops = {
             flesh => 1,
-            flesh_fields => {acqlid => []}
+            flesh_fields => {acqlid => ["cancel_reason"]} #XXX cancel_reason? always? really?
         };
         push(@{$ops->{flesh_fields}->{acqlid}}, 'fund') if $$options{flesh_fund};
         push(@{$ops->{flesh_fields}->{acqlid}}, 'fund_debit') if $$options{flesh_fund_debit};

Modified: trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm	2010-03-15 08:52:29 UTC (rev 15844)
@@ -2120,52 +2120,348 @@
 }
 
 
+sub not_cancelable {
+    my $o = shift;
+    (ref $o eq "HASH" and $o->{"textcode"} eq "ACQ_NOT_CANCELABLE");
+}
+
 __PACKAGE__->register_method(
-	method => 'cancel_lineitem_api',
-	api_name	=> 'open-ils.acq.lineitem.cancel',
+	method => "cancel_purchase_order_api",
+	api_name	=> "open-ils.acq.purchase_order.cancel",
 	signature => {
+        desc => q/Cancels an on-order purchase order/,
+        params => [
+            {desc => "Authentication token", type => "string"},
+            {desc => "PO ID to cancel", type => "number"},
+            {desc => "Cancel reason ID", type => "number"}
+        ],
+        return => {desc => q/Object describing changed POs, LIs and LIDs
+            on success; Event on error./}
+    }
+);
+
+sub cancel_purchase_order_api {
+    my ($self, $conn, $auth, $po_id, $cancel_reason) = @_;
+
+    my $e = new_editor("xact" => 1, "authtoken" => $auth);
+    return $e->die_event unless $e->checkauth;
+    my $mgr = new OpenILS::Application::Acq::BatchManager(
+        "editor" => $e, "conn" => $conn
+    );
+
+    $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
+        return new OpenILS::Event(
+            "BAD_PARAMS", "note" => "Provide cancel reason ID"
+        );
+
+    my $result = cancel_purchase_order($mgr, $po_id, $cancel_reason) or
+        return $e->die_event;
+    if (not_cancelable($result)) { # event not from CStoreEditor
+        $e->rollback;
+        return $result;
+    } elsif ($result == -1) {
+        $e->rollback;
+        return new OpenILS::Event("ACQ_ALREADY_CANCELED");
+    }
+
+    $e->commit or return $e->die_event;
+
+    # XXX create purchase order status events?
+    return $result;
+}
+
+sub cancel_purchase_order {
+    my ($mgr, $po_id, $cancel_reason) = @_;
+
+    my $po = $mgr->editor->retrieve_acq_purchase_order($po_id) or return 0;
+
+    # XXX is "cancelled" a typo?  It's not correct US spelling, anyway.
+    # Depending on context, this may not warrant an event.
+    return -1 if $po->state eq "cancelled";
+
+    # But this always does.
+    return new OpenILS::Event(
+        "ACQ_NOT_CANCELABLE", "note" => "purchase_order $po_id"
+    ) unless ($po->state eq "on-order" or $po->state eq "pending");
+
+    return 0 unless
+        $mgr->editor->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
+
+    $po->state("cancelled");
+    $po->cancel_reason($cancel_reason);
+
+    my $li_ids = $mgr->editor->search_acq_lineitem(
+        {"purchase_order" => $po_id}, {"idlist" => 1}
+    );
+
+    my $result = {"li" => {}, "lid" => {}};
+    foreach my $li_id (@$li_ids) {
+        my $li_result = cancel_lineitem($mgr, $li_id, $cancel_reason)
+            or return 0;
+
+        next if $li_result == -1; # already canceled:skip.
+        return $li_result if not_cancelable($li_result); # not cancelable:stop.
+
+        # Merge in each LI result (there's only going to be
+        # one per call to cancel_lineitem).
+        my ($k, $v) = each %{$li_result->{"li"}};
+        $result->{"li"}->{$k} = $v;
+
+        # Merge in each LID result (there may be many per call to
+        # cancel_lineitem).
+        while (($k, $v) = each %{$li_result->{"lid"}}) {
+            $result->{"lid"}->{$k} = $v;
+        }
+    }
+
+    # TODO who/what/where/how do we indicate this change for electronic orders?
+    # TODO return changes to encumbered/spent
+    # TODO maybe cascade up from smaller object to container object if last
+    # smaller object in the container has been canceled?
+
+    update_purchase_order($mgr, $po) or return 0;
+    $result->{"po"} = {
+        $po_id => {"state" => $po->state, "cancel_reason" => $cancel_reason}
+    };
+    return $result;
+}
+
+
+__PACKAGE__->register_method(
+	method => "cancel_lineitem_api",
+	api_name	=> "open-ils.acq.lineitem.cancel",
+	signature => {
         desc => q/Cancels an on-order lineitem/,
         params => [
-            {desc => 'Authentication token', type => 'string'},
-            {desc => 'Lineitem ID to cancel', type => 'number'},
-            {desc => 'Cancel Cause ID', type => 'number'}
+            {desc => "Authentication token", type => "string"},
+            {desc => "Lineitem ID to cancel", type => "number"},
+            {desc => "Cancel reason ID", type => "number"}
         ],
-        return => {desc => '1 on success, Event on error'}
+        return => {desc => q/Object describing changed LIs and LIDs on success;
+            Event on error./}
     }
 );
 
+__PACKAGE__->register_method(
+	method => "cancel_lineitem_api",
+	api_name	=> "open-ils.acq.lineitem.cancel.batch",
+	signature => {
+        desc => q/Batched version of open-ils.acq.lineitem.cancel/,
+        return => {desc => q/Object describing changed LIs and LIDs on success;
+            Event on error./}
+    }
+);
+
 sub cancel_lineitem_api {
-    my($self, $conn, $auth, $li_id, $cancel_cause) = @_;
+    my ($self, $conn, $auth, $li_id, $cancel_reason) = @_;
 
-    my $e = new_editor(xact=>1, authtoken=>$auth);
+    my $batched = $self->api_name =~ /\.batch/;
+
+    my $e = new_editor("xact" => 1, "authtoken" => $auth);
     return $e->die_event unless $e->checkauth;
-    my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
+    my $mgr = new OpenILS::Application::Acq::BatchManager(
+        "editor" => $e, "conn" => $conn
+    );
 
-    my $li = $e->retrieve_acq_lineitem([$li_id, 
-        {flesh => 1, flesh_fields => {jub => [q/purchase_order/]}}]);
+    $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
+        return new OpenILS::Event(
+            "BAD_PARAMS", "note" => "Provide cancel reason ID"
+        );
 
-    unless( $li->purchase_order and ($li->state eq 'on-order' or $li->state eq 'pending-order') ) {
+    my ($result, $maybe_event);
+
+    if ($batched) {
+        $result = {"li" => {}, "lid" => {}};
+        foreach my $one_li_id (@$li_id) {
+            my $one = cancel_lineitem($mgr, $one_li_id, $cancel_reason) or
+                return $e->die_event;
+            if (not_cancelable($one)) {
+                $maybe_event = $one;
+            } elsif ($result == -1) {
+                $maybe_event = new OpenILS::Event("ACQ_ALREADY_CANCELED");
+            } else {
+                my ($k, $v);
+                if ($one->{"li"}) {
+                    while (($k, $v) = each %{$one->{"li"}}) {
+                        $result->{"li"}->{$k} = $v;
+                    }
+                }
+                if ($one->{"lid"}) {
+                    while (($k, $v) = each %{$one->{"lid"}}) {
+                        $result->{"lid"}->{$k} = $v;
+                    }
+                }
+            }
+        }
+    } else {
+        $result = cancel_lineitem($mgr, $li_id, $cancel_reason) or
+            return $e->die_event;
+
+        if (not_cancelable($result)) {
+            $e->rollback;
+            return $result;
+        } elsif ($result == -1) {
+            $e->rollback;
+            return new OpenILS::Event("ACQ_ALREADY_CANCELED");
+        }
+    }
+
+    if ($batched and not scalar keys %{$result->{"li"}}) {
         $e->rollback;
-        return OpenILS::Event->new('BAD_PARAMS') 
+        return $maybe_event;
+    } else {
+        $e->commit or return $e->die_event;
+        # create_lineitem_status_events should handle array li_id ok
+        create_lineitem_status_events($mgr, $li_id, "aur.cancelled");
+        return $result;
     }
+}
 
-    return $e->die_event unless 
-        $e->allowed('CREATE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
+sub cancel_lineitem {
+    my ($mgr, $li_id, $cancel_reason) = @_;
+    my $li = $mgr->editor->retrieve_acq_lineitem([
+        $li_id, {"flesh" => 1, "flesh_fields" => {"jub" => ["purchase_order"]}}
+    ]) or return 0;
 
-    $li->state('cancelled');
+    return 0 unless $mgr->editor->allowed(
+        "CREATE_PURCHASE_ORDER", $li->purchase_order->ordering_agency
+    );
 
+    # Depending on context, this may not warrant an event.
+    return -1 if $li->state eq "cancelled";
+
+    # But this always does.
+    return new OpenILS::Event(
+        "ACQ_NOT_CANCELABLE", "note" => "lineitem $li_id"
+    ) unless (
+        $li->purchase_order and (
+            $li->state eq "on-order" or $li->state eq "pending-order"
+        )
+    );
+
+    $li->state("cancelled");
+    $li->cancel_reason($cancel_reason);
+
+    my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
+        {"lineitem" => $li_id}, {"idlist" => 1}
+    );
+
+    my $result = {"lid" => {}};
+    foreach my $lid_id (@$lid_ids) {
+        my $lid_result = cancel_lineitem_detail($mgr, $lid_id, $cancel_reason)
+            or return 0;
+
+        next if $lid_result == -1; # already canceled: just skip it.
+        return $lid_result if not_cancelable($lid_result); # not cxlable: stop.
+
+        # Merge in each LID result (there's only going to be one per call to
+        # cancel_lineitem_detail).
+        my ($k, $v) = each %{$lid_result->{"lid"}};
+        $result->{"lid"}->{$k} = $v;
+    }
+
     # TODO delete the associated fund debits?
-    # TODO add support for cancel reasons
     # TODO who/what/where/how do we indicate this change for electronic orders?
 
-    update_lineitem($mgr, $li) or return $e->die_event;
-    $e->commit;
+    update_lineitem($mgr, $li) or return 0;
+    $result->{"li"} = {
+        $li_id => {
+            "state" => $li->state,
+            "cancel_reason" => $cancel_reason
+        }
+    };
+    return $result;
+}
 
-    $conn->respond_complete($li);
-    create_lineitem_status_events($mgr, $li_id, 'aur.cancelled');
-    return undef;
+
+__PACKAGE__->register_method(
+	method => "cancel_lineitem_detail_api",
+	api_name	=> "open-ils.acq.lineitem_detail.cancel",
+	signature => {
+        desc => q/Cancels an on-order lineitem detail/,
+        params => [
+            {desc => "Authentication token", type => "string"},
+            {desc => "Lineitem detail ID to cancel", type => "number"},
+            {desc => "Cancel reason ID", type => "number"}
+        ],
+        return => {desc => q/Object describing changed LIDs on success;
+            Event on error./}
+    }
+);
+
+sub cancel_lineitem_detail_api {
+    my ($self, $conn, $auth, $lid_id, $cancel_reason) = @_;
+
+    my $e = new_editor("xact" => 1, "authtoken" => $auth);
+    return $e->die_event unless $e->checkauth;
+    my $mgr = new OpenILS::Application::Acq::BatchManager(
+        "editor" => $e, "conn" => $conn
+    );
+
+    $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
+        return new OpenILS::Event(
+            "BAD_PARAMS", "note" => "Provide cancel reason ID"
+        );
+
+    my $result = cancel_lineitem_detail($mgr, $lid_id, $cancel_reason) or
+        return $e->die_event;
+
+    if (not_cancelable($result)) {
+        $e->rollback;
+        return $result;
+    } elsif ($result == -1) {
+        $e->rollback;
+        return new OpenILS::Event("ACQ_ALREADY_CANCELED");
+    }
+
+    $e->commit or return $e->die_event;
+
+    # XXX create lineitem detail status events?
+    return $result;
 }
 
+sub cancel_lineitem_detail {
+    my ($mgr, $lid_id, $cancel_reason) = @_;
+    my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
+        $lid_id, {
+            "flesh" => 2,
+            "flesh_fields" => {
+                "acqlid" => ["lineitem"], "jub" => ["purchase_order"]
+            }
+        }
+    ]) or return 0;
+
+    # Depending on context, this may not warrant an event.
+    return -1 if $lid->cancel_reason;
+
+    # But this always does.
+    return new OpenILS::Event(
+        "ACQ_NOT_CANCELABLE", "note" => "lineitem_detail $lid_id"
+    ) unless (
+        (not $lid->recv_time) and
+        $lid->lineitem and
+        $lid->lineitem->purchase_order and (
+            $lid->lineitem->state eq "on-order" or
+            $lid->lineitem->state eq "pending-order"
+        )
+    );
+
+    return 0 unless $mgr->editor->allowed(
+        "CREATE_PURCHASE_ORDER",
+        $lid->lineitem->purchase_order->ordering_agency
+    );
+
+    $lid->cancel_reason($cancel_reason);
+
+    # TODO who/what/where/how do we indicate this change for electronic orders?
+
+    # XXX LIDs don't have either an editor or a edit_time field. Should we
+    # update these on the LI when we alter an LID?
+    $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
+    return {"lid" => {$lid_id => {"cancel_reason" => $cancel_reason}}};
+}
+
+
 __PACKAGE__->register_method (
     method        => 'user_requests',
     api_name    => 'open-ils.acq.user_request.retrieve.by_user_id',

Modified: trunk/Open-ILS/web/js/dojo/openils/acq/nls/acq.js
===================================================================
--- trunk/Open-ILS/web/js/dojo/openils/acq/nls/acq.js	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/web/js/dojo/openils/acq/nls/acq.js	2010-03-15 08:52:29 UTC (rev 15844)
@@ -27,5 +27,10 @@
     'ITS_YOU': "You",
     'JUST_NOW': "Just now",
     'EXPLAIN_DFA_MGMT': "Remove record of this distribution formula usage?",
-    'VENDOR_PUBLIC': "VENDOR PUBLIC"
+    'VENDOR_PUBLIC': "VENDOR PUBLIC",
+    'PO_CANCEL_CONFIRM': "Are you SURE you want to cancel this purchase order?",
+    'LI_CANCEL_CONFIRM': "Are you SURE you want to cancel this line item?",
+    'LID_CANCEL_CONFIRM': "Are you SURE you want to cancel this copy?",
+    'CANCEL_REASON': "Cancel reason",
+    'CANCEL': "Cancel"
 }

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-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js	2010-03-15 08:52:29 UTC (rev 15844)
@@ -252,16 +252,13 @@
         countNode.innerHTML = count;
         countNode.id = 'acq-lit-copy-count-label-' + li.id();
 
-        // lineitem state
-        nodeByName('li_state', row).innerHTML = li.state(); // TODO i18n state labels
-        
         // lineitem price
         var priceInput = dojo.query('[name=price]', row)[0];
         priceInput.value = li.estimated_unit_price() || '';
         priceInput.onchange = function() { self.updateLiPrice(priceInput, li) };
 
         // show either "mark received" or "unreceive" as appropriate
-        this.updateLiReceivedness(li, row);
+        this.updateLiState(li, row);
 
         if (!skip_final_placement) {
             self.tbody.appendChild(row);
@@ -287,7 +284,8 @@
         nodeByName("notes_count", row).innerHTML = li.lineitem_notes().length;
     };
 
-    this.updateLiReceivedness = function(li, row) {
+    /* XXX NOT related to _updateLiState(). rethink */
+    this.updateLiState = function(li, row) {
         if (typeof(row) == "undefined")
             row = dojo.query('tr[li="' + li.id() + '"]', "acq-lit-tbody")[0];
 
@@ -295,7 +293,27 @@
         var unrecv_link = nodeByName("unreceive_link", row);
         var real_copies_link = nodeByName("real_copies_link", row);
         var holdings_maintenance_link = nodeByName("holdings_maintenance_link", row);
+        var state_cell = nodeByName("li_state", row);
 
+        if (li.state() == "cancelled") {
+            var holds_state = dojo.create(
+                "span", {
+                    "style": "border-bottom: 1px dashed #000;",
+                    "innerHTML": li.state()
+                }, state_cell, "only"
+            );
+            new dijit.Tooltip(
+                {
+                    "label": "<em>" + li.cancel_reason().label() +
+                        "</em><br />" + li.cancel_reason().description(),
+                    "connectId": [holds_state]
+                }, dojo.create("span", null, state_cell, "last")
+            );
+        } else {
+            state_cell.innerHTML = li.state(); // TODO i18n state labels
+        }
+
+
         /* handle row coloring for based on LI state */
         openils.Util.removeCSSClass(row, /^oils-acq-li-state-/);
         openils.Util.addCSSClass(row, "oils-acq-li-state-" + li.state());
@@ -322,11 +340,11 @@
                     };
                     // 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);
+                    openils.Util.show(real_copies_link, "inline");
                     real_copies_link.onclick = function() {
                         self.showRealCopyEditUI(li);
                     }
-                    openils.Util.show(holdings_maintenance_link);
+                    openils.Util.show(holdings_maintenance_link, "inline");
                     holdings_maintenance_link.onclick = self.generateMakeRecTab( li.eg_bib_id(), 'copy_browser' );
                     return;
             }
@@ -583,6 +601,7 @@
 
                 params: [self.authtoken, liId, {
                     flesh_attrs: true,
+                    flesh_cancel_reason: true,
                     flesh_li_details: true,
                     flesh_fund_debit: true }],
 
@@ -1219,10 +1238,10 @@
             }
         );
 
-        this.updateLidReceivedness(copy, row);
+        this.updateLidState(copy, row);
     };
 
-    this.updateLidReceivedness = function(copy, row) {
+    this.updateLidState = function(copy, row) {
         if (typeof(row) == "undefined") {
             row = dojo.query(
                 'tr[copy_id="' + copy.id() + '"]', this.copyTbody
@@ -1233,40 +1252,124 @@
         var recv_link = nodeByName("receive", row);
         var unrecv_link = nodeByName("unreceive", row);
         var del_link = nodeByName("delete", row);
+        var cxl_link = nodeByName("cancel", row);
+        var cxl_reason_link = nodeByName("cancel_reason", row);
 
-        if (this.isPO) {
+        if (copy.cancel_reason()) {
             openils.Util.hide(del_link.parentNode);
+            openils.Util.hide(recv_link);
+            openils.Util.hide(unrecv_link);
+            openils.Util.hide(cxl_link);
 
-            /* Avoid showing (un)receive links for virtual copies */
+            /* XXX the following may leak memory in a long lived table: dijits may not get destroyed... not positive. revisit. */
+            var holds_reason = dojo.create(
+                "span", {
+                    "style": "border-bottom: 1px dashed #000;",
+                    "innerHTML": "Cancelled" /* XXX [sic] and i18n */
+                }, cxl_reason_link, "only"
+            );
+            new dijit.Tooltip(
+                {
+                    "label": "<em>" + copy.cancel_reason().label() +
+                        "</em><br />" + copy.cancel_reason().description(),
+                    "connectId": [holds_reason]
+                }, dojo.create("span", null, cxl_reason_link, "last")
+            );
+            openils.Util.show(cxl_reason_link, "inline");
+        } else if (this.isPO) {
+            openils.Util.hide(del_link.parentNode);
+            openils.Util.hide(cxl_reason_link);
+
+            /* Avoid showing (un)receive links, cancel links, for virt copies */
             if (copy.id() > 0) {
                 if(copy.recv_time()) {
+                    openils.Util.hide(cxl_link);
                     openils.Util.hide(recv_link);
-                    openils.Util.show(unrecv_link);
+                    openils.Util.show(unrecv_link, "inline");
                     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);
+                    openils.Util.show(recv_link, "inline");
+                    openils.Util.show(cxl_link, "inline");
                     recv_link.onclick = function() {
                         if (self.checkLiAlerts(copy.lineitem()))
                             self.issueReceive(copy);
                     };
+                    cxl_link.onclick = function() {
+                        self.cancelLid(copy.id());
+                    };
                 }
             } else {
+                openils.Util.hide(cxl_link);
                 openils.Util.hide(unrecv_link);
                 openils.Util.hide(recv_link);
             }
         } else {
             openils.Util.hide(unrecv_link);
             openils.Util.hide(recv_link);
+            openils.Util.hide(cxl_reason_link);
 
             del_link.onclick = function() { self.deleteCopy(row) };
             openils.Util.show(del_link.parentNode);
         }
     }
 
+    this.cancelLid = function(lid_id) {
+        lidCancelDialog._lid_id = lid_id;
+        openils.Util.show(lidCancelDialog.domNode.parentNode);
+        lidCancelDialog.show();
+        if (!lidCancelDialog._prepared) {
+            var widget = new openils.widget.AutoFieldWidget({
+                "fmField": "cancel_reason",
+                "fmClass": "acqlid",
+                "parentNode": dojo.byId("acq-lit-lid-cancel-reason"),
+                "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
+                "forceSync": true
+            });
+            widget.build(
+                function(w, ww) {
+                    acqLidCancelButton.onClick = function() {
+                        if (w.attr("value")) {
+                            if (confirm(localeStrings.LID_CANCEL_CONFIRM)) {
+                                self._cancelLid(
+                                    lidCancelDialog._lid_id,
+                                    w.attr("value")
+                                );
+                            }
+                            lidCancelDialog.hide();
+                        }
+                    };
+                    lidCancelDialog._prepared = true;
+                }
+            );
+        }
+    };
+
+    this._cancelLid = function(lid_id, reason) {
+        fieldmapper.standardRequest(
+            ["open-ils.acq", "open-ils.acq.lineitem_detail.cancel"], {
+                "params": [openils.User.authtoken, lid_id, reason],
+                "async": true,
+                "onresponse": function(r) {
+                    if (r = openils.Util.readResponse(r)) {
+                        if (r.lid) {
+                            for (var id in r.lid) {
+                                /* actually this should only iterate once */
+                                self.copyCache[id].cancel_reason(
+                                    r.lid[id].cancel_reason
+                                );
+                                self.updateLidState(self.copyCache[id]);
+                            }
+                        }
+                    }
+                }
+            }
+        );
+    };
+
     this._confirmAlert = function(li, lin) {
         return confirm(
             dojo.string.substitute(
@@ -1452,6 +1555,12 @@
                     location.href = oilsBasePath + '/acq/picklist/brief_record?po=' + this.isPO;
                 else
                     location.href = oilsBasePath + '/acq/picklist/brief_record?pl=' + this.isPL;
+
+                break;
+
+            case "cancel_lineitems":
+                this.maybeCancelLineitems();
+                break;
         }
     }
 
@@ -1472,6 +1581,65 @@
         );
     }
 
+    this.maybeCancelLineitems = function() {
+        openils.Util.show("acq-lit-cancel-reason", "inline");
+        if (!acqLitCancelLineitemsButton._prepared) {
+            var widget = new openils.widget.AutoFieldWidget({
+                "fmField": "cancel_reason",
+                "fmClass": "jub",
+                "parentNode": dojo.byId("acq-lit-cancel-reason-selector"),
+                "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
+                "forceSync": true
+            });
+            widget.build(
+                function(w, ww) {
+                    acqLitCancelLineitemsButton.onClick = function() {
+                        if (w.attr("value")) {
+                            if (confirm(localeStrings.LI_CANCEL_CONFIRM)) {
+                                self._cancelLineitems(w.attr("value"));
+                            }
+                            openils.Util.hide("acq-lit-cancel-reason");
+                        }
+                    };
+                    acqLitCancelLineitemsButton._prepared = true;
+                }
+            );
+        }
+    };
+
+    this._cancelLineitems = function(reason) {
+        var id_list = this.getSelected().map(function(o) { return o.id(); });
+        fieldmapper.standardRequest(
+            ["open-ils.acq", "open-ils.acq.lineitem.cancel.batch"], {
+                "params": [openils.User.authtoken, id_list, reason],
+                "async": true,
+                "onresponse": function(r) {
+                    if (r = openils.Util.readResponse(r)) {
+                        if (r.li) {
+                            for (var id in r.li) {
+                                self.liCache[id].state(r.li[id].state);
+                                self.liCache[id].cancel_reason(
+                                    r.li[id].cancel_reason
+                                );
+                                self.updateLiState(self.liCache[id]);
+                            }
+                        }
+                        if (r.lid && self.copyCache) {
+                            for (var id in r.lid) {
+                                if (self.copyCache[id]) {
+                                    self.copyCache[id].cancel_reason(
+                                        r.lid[id].cancel_reason
+                                    );
+                                    self.updateLidState(self.copyCache[id]);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        );
+    };
+
     this.chooseExportAttr = function() {
         if (!acqLitExportAttrSelector._li_setup) {
             var self = this;
@@ -1590,8 +1758,8 @@
                 "params": [this.authtoken, obj.id()],
                 "onresponse": function(r) {
                     self.handleReceive(openils.Util.readResponse(r));
-                },
-                "oncomplete": function() { progressDialog.hide(); }
+                    progressDialog.hide();
+                }
             }
         );
     };
@@ -1605,7 +1773,7 @@
                 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.updateLiReceivedness(self.liCache[li_id]);
+                    self.updateLiState(self.liCache[li_id]);
                 }
             }
             if (resp.po) {
@@ -1616,7 +1784,7 @@
                 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]);
+                    self.updateLidState(self.copyCache[lid_id]);
                 }
             }
         }

Modified: trunk/Open-ILS/web/js/ui/default/acq/picklist/view.js
===================================================================
--- trunk/Open-ILS/web/js/ui/default/acq/picklist/view.js	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/web/js/ui/default/acq/picklist/view.js	2010-03-15 08:52:29 UTC (rev 15844)
@@ -78,7 +78,7 @@
         ['open-ils.acq', 'open-ils.acq.lineitem.picklist.retrieve'],
         {   async: true,
             params: [openils.User.authtoken, plId, 
-                {flesh_notes:true, flesh_attrs:true, clear_marc:true, offset:plOffset, limit:plLimit}],
+                {flesh_notes:true, flesh_cancel_reason:true, flesh_attrs:true, clear_marc:true, offset:plOffset, limit:plLimit}],
             onresponse: function(r) {
                 var li = openils.Util.readResponse(r);
                 if (li) { /* Not every response is an LI (for some reason) */

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-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/web/js/ui/default/acq/po/view_po.js	2010-03-15 08:52:29 UTC (rev 15844)
@@ -3,6 +3,7 @@
 dojo.require('openils.Util');
 dojo.require('openils.PermaCrud');
 
+var pcrud = new openils.PermaCrud();
 var PO = null;
 var liTable;
 var poNoteTable;
@@ -162,6 +163,82 @@
     }
 }
 
+function cancellationUpdater(r) {
+    var r = openils.Util.readResponse(r);
+    if (r) {
+        if (r.po) updatePoState(r.po);
+        if (r.li) {
+            for (var id in r.li) {
+                liTable.liCache[id].state(r.li[id].state);
+                liTable.liCache[id].cancel_reason(r.li[id].cancel_reason);
+                liTable.updateLiState(liTable.liCache[id]);
+            }
+        }
+        if (r.lid && liTable.copyCache) {
+            for (var id in r.lid) {
+                if (liTable.copyCache[id]) {
+                    liTable.copyCache[id].cancel_reason(
+                        r.lid[id].cancel_reason
+                    );
+                    liTable.updateLidState(liTable.copyCache[id]);
+                }
+            }
+        }
+    }
+}
+
+function makeCancelWidget(node, labelnode) {
+    openils.Util.hide("acq-po-choose-cancel-reason");
+
+    if (PO.cancel_reason()) {
+        labelnode.innerHTML = localeStrings.CANCEL_REASON;
+        node.innerHTML = PO.cancel_reason().description() + " (" +
+            PO.cancel_reason().label() + ")";
+    } else if (["on-order", "pending"].indexOf(PO.state()) == -1) {
+        dojo.destroy(this.oldTip);
+        labelnode.innerHTML = "";
+        node.innerHTML = "";
+    } else {
+        dojo.destroy(this.oldTip);
+        labelnode.innerHTML = localeStrings.CANCEL;
+        node.innerHTML = "";
+        if (!acqPoCancelReasonSubmit._prepared) {
+            var widget = new openils.widget.AutoFieldWidget({
+                "fmField": "cancel_reason",
+                "fmClass": "acqpo",
+                "parentNode": dojo.byId("acq-po-cancel-reason"),
+                "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
+                "forceSync": true
+            });
+            widget.build(
+                function(w, ww) {
+                    acqPoCancelReasonSubmit.onClick = function() {
+                        if (w.attr("value")) {
+                            if (confirm(localeStrings.PO_CANCEL_CONFIRM)) {
+                                fieldmapper.standardRequest(
+                                    ["open-ils.acq",
+                                        "open-ils.acq.purchase_order.cancel"],
+                                    {
+                                        "params": [
+                                            openils.User.authtoken,
+                                            PO.id(), 
+                                            w.attr("value")
+                                        ],
+                                        "async": true,
+                                        "oncomplete": cancellationUpdater
+                                    }
+                                );
+                            }
+                        }
+                    };
+                    acqPoCancelReasonSubmit._prepared = true;
+                }
+            );
+        }
+        openils.Util.show("acq-po-choose-cancel-reason", "inline");
+    }
+}
+
 function renderPo() {
     dojo.byId("acq-po-view-id").innerHTML = PO.id();
     dojo.byId("acq-po-view-name").innerHTML = PO.name();
@@ -169,6 +246,10 @@
     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
+    makeCancelWidget(
+        dojo.byId("acq-po-view-cancel-reason"),
+        dojo.byId("acq-po-cancel-label")
+    );
     dojo.byId("acq-po-view-notes").innerHTML = PO.notes().length;
 
     if(PO.state() == "pending") {
@@ -208,7 +289,7 @@
     fieldmapper.standardRequest(
         ['open-ils.acq', 'open-ils.acq.lineitem.search'],
         {   async: true,
-            params: [openils.User.authtoken, {purchase_order:poId}, {flesh_attrs:true, flesh_notes:true}],
+params: [openils.User.authtoken, {purchase_order:poId}, {flesh_attrs:true, flesh_notes:true, flesh_cancel_reason:true}],
             onresponse: function(r) {
                 liTable.show('list');
                 liTable.addLineitem(openils.Util.readResponse(r));
@@ -264,7 +345,6 @@
     var value = prompt('Enter new purchase order name:', PO.name()); // TODO i18n
     if(!value || value == PO.name()) return;
     PO.name(value);
-    var pcrud = new openils.PermaCrud();
     pcrud.update(PO, {
         oncomplete : function(r, cudResults) {
             var stat = cudResults[0];

Modified: trunk/Open-ILS/web/opac/locale/en-US/lang.dtd
===================================================================
--- trunk/Open-ILS/web/opac/locale/en-US/lang.dtd	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/web/opac/locale/en-US/lang.dtd	2010-03-15 08:52:29 UTC (rev 15844)
@@ -688,6 +688,8 @@
 <!ENTITY staff.main.menu.admin.server_admin.acq.accesskey "A">
 <!ENTITY staff.main.menu.admin.server_admin.acq.lineitem_alert.label "Line Item Alerts">
 <!ENTITY staff.main.menu.admin.server_admin.acq.lineitem_alert.accesskey "L">
+<!ENTITY staff.main.menu.admin.server_admin.acq.cancel_reason.label "Cancel Reasons">
+<!ENTITY staff.main.menu.admin.server_admin.acq.cancel_reason.accesskey "C">
 
 <!ENTITY staff.main.menu.admin.server_admin.booking.label "Booking">
 <!ENTITY staff.main.menu.admin.server_admin.booking.accesskey "B">

Modified: trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2
===================================================================
--- trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/web/templates/default/acq/common/li_table.tt2	2010-03-15 08:52:29 UTC (rev 15844)
@@ -22,6 +22,7 @@
                                             <option mask='po' value='' disabled='disabled'>----PO----</option>
                                             <option mask='sr|pl' value='create_order'>Create Purchase Order</option>
                                             <option mask='po' value='create_assets'>Load Bibs and Items</option>
+                                            <option mask='po' value='cancel_lineitems'>Cancel Selected Lineitems</option>
                                             <option mask='po' value='receive_po'>Mark Purchase Order as Received</option>
                                             <option mask='po' value='rollback_receive_po'>Un-Receive Purchase Order</option>
                                             <option mask='po' value='print_po'>Print Purchase Order</option>
@@ -30,6 +31,10 @@
                                             <input dojoType="dijit.form.FilteringSelect" id="acq-lit-export-attr" jsId="acqLitExportAttrSelector" labelAttr="description" searchAttr="description" />
                                             <span dojoType="dijit.form.Button" jsId="acqLitExportAttrButton">Export List</span>
                                         </span>
+                                        <span id="acq-lit-cancel-reason" class="hidden">
+                                            <span id="acq-lit-cancel-reason-selector"></span>
+                                            <span dojoType="dijit.form.Button" jsId="acqLitCancelLineitemsButton">Cancel Line Items</span>
+                                        </span>
                                     </span>
                                     <span id='acq-lit-generic-progress' class='hidden'>
                                         <span dojoType="dijit.ProgressBar" style="width:300px" jsId="litGenericProgress"></span>
@@ -91,8 +96,7 @@
                             </tbody>
                         </table>
                     </td>
-                    <td><a class='hidden' name='real_copies_link' href='javascript:void(0);'>Update&nbsp;Barcodes</a></td>
-                    <td><a class='hidden' name='holdings_maintenance_link' href='javascript:void(0);'>Holdings&nbsp;Maintenance</a></td>
+                    <td><a class='hidden' name='real_copies_link' href='javascript:void(0);'>Update&nbsp;Barcodes</a>&nbsp;&nbsp;<a class='hidden' name='holdings_maintenance_link' href='javascript:void(0);'>Holdings&nbsp;Maintenance</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>
@@ -205,7 +209,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><a href='javascript:void(0);' name='unreceive'>Un-Receive</a></td>
+                    <td><a href='javascript:void(0);' name='receive'>Mark&nbsp;Received</a><a href='javascript:void(0);' name='unreceive'>Un-Receive</a>&nbsp;<a href="javascript:void(0);" name='cancel'>Cancel</a><span class="hidden" name='cancel_reason'></span></td>
                     <td><div name='delete' dojoType='dijit.form.Button' style='color:red;'>X</div></td>
                 </tr>
             </tbody>
@@ -348,6 +352,15 @@
         </table>
     </div>
 
+    <div class="hidden">
+        <div dojoType="dijit.Dialog" jsId="lidCancelDialog">
+            <label for="acq-lit-lid-cancel-reason">Reason:</label>
+            <span id="acq-lit-lid-cancel-reason"></span>
+            <span dojoType="dijit.form.Button"
+                jsId="acqLidCancelButton">Cancel Copy</span>
+        </div>
+    </div>
+
     <div dojoType="dijit.Dialog" jsId='acqLitChangeLiStateDialog'>
         <table class='dijitTooltipTable'>
             <tr>

Modified: trunk/Open-ILS/web/templates/default/acq/po/view.tt2
===================================================================
--- trunk/Open-ILS/web/templates/default/acq/po/view.tt2	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/web/templates/default/acq/po/view.tt2	2010-03-15 08:52:29 UTC (rev 15844)
@@ -14,6 +14,26 @@
                 <tr><td>Total Spent</td><td>$<span id='acq-po-view-total-spent'/></td></tr>
                 <tr><td>Status</td><td><span id='acq-po-view-state'/></td></tr>
                 <tr>
+                    <td id="acq-po-cancel-label"></td>
+                    <td>
+                        <span id="acq-po-view-cancel-reason"></span>
+                        <span id="acq-po-choose-cancel-reason" class="hidden">
+                            <span dojoType="dijit.form.DropDownButton">
+                                <span>Cancel order</span>
+                                <span dojoType="dijit.TooltipDialog">
+                                    <label for="acq-po-cancel-reason">
+                                        Reason:
+                                    </label>
+                                    <span id="acq-po-cancel-reason"></span>
+                                    <button jsId="acqPoCancelReasonSubmit"
+                                        dojoType="dijit.form.Button"
+                                        type="submit">Cancel order</button>
+                                </span>
+                            </span>
+                        </span>
+                    </td>
+                </tr>
+                <tr>
                     <td>Notes</td>
                     <td>
                         <a href="javascript:void(0);"

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js	2010-03-15 08:52:29 UTC (rev 15844)
@@ -623,6 +623,10 @@
                 ['oncommand'],
                 function() { open_eg_web_page('conify/global/acq/lineitem_alert'); }
             ],
+            'cmd_server_admin_acq_cancel_reason' : [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/acq/cancel_reason'); }
+            ],
             'cmd_server_admin_z39_source' : [
                 ['oncommand'],
                 function() { open_eg_web_page('conify/global/config/z3950_source'); }

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul	2010-03-15 01:54:50 UTC (rev 15843)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul	2010-03-15 08:52:29 UTC (rev 15844)
@@ -127,6 +127,7 @@
     <command id="cmd_server_admin_copy_status"/>
     <command id="cmd_server_admin_marc_code"/>
     <command id="cmd_server_admin_billing_type"/>
+    <command id="cmd_server_admin_acq_cancel_reason"/>
     <command id="cmd_server_admin_acq_lineitem_alert"/>
     <command id="cmd_server_admin_z39_source"/>
     <command id="cmd_server_admin_circ_mod"/>
@@ -348,6 +349,7 @@
                 <menu id="main.menu.admin.server.acq" label="&staff.main.menu.admin.server_admin.acq.label;" accesskey="&staff.main.menu.admin.server_admin.acq.accesskey;">
                     <menupopup id="main.menu.admin.server.acq.popup">
                         <menuitem label="&staff.main.menu.admin.server_admin.acq.lineitem_alert.label;" accesskey="&staff.main.menu.admin.server_admin.acq.lineitem_alert.accesskey;" command="cmd_server_admin_acq_lineitem_alert"/>
+                        <menuitem label="&staff.main.menu.admin.server_admin.acq.cancel_reason.label;" accesskey="&staff.main.menu.admin.server_admin.acq.cancel_reason.accesskey;" command="cmd_server_admin_acq_cancel_reason"/>
                     </menupopup>
                 </menu>
                 <menu id="main.menu.admin.server.booking" label="&staff.main.menu.admin.server_admin.booking.label;" accesskey="&staff.main.menu.admin.server_admin.booking.accesskey;">



More information about the open-ils-commits mailing list