LP#1440114 Blanket order PO "finalize"
authorBill Erickson <berickxx@gmail.com>
Fri, 10 Apr 2015 16:30:22 +0000 (12:30 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 19 Aug 2015 16:33:28 +0000 (12:33 -0400)
When invoicing a PO that has at least one blanket charge, a new option is
present which allows staff to indicate that an invoice is the final invoice
for the PO.  Finalizing a PO results in the following:

1. Encumbrances for all blanket charges on the PO are dropped to $0.
   This is done by setting the amount paid in the original fund_debit
   (linked the blanket po_item) to $0.

2. If no pending lineitems exist on the PO, the PO is marked as received.
   If there are pending lineitems, the state is left untouched.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm
Open-ILS/src/templates/acq/invoice/view.tt2
Open-ILS/web/css/skin/default/acq.css
Open-ILS/web/js/dojo/openils/acq/nls/acq.js
Open-ILS/web/js/ui/default/acq/invoice/view.js

index aa3bf49..3980e62 100644 (file)
@@ -38,6 +38,7 @@ __PACKAGE__->register_method(
             {desc => q/Invoice/, type => 'number'},
             {desc => q/Entries.  Array of 'acqie' objects/, type => 'array'},
             {desc => q/Items.  Array of 'acqii' objects/, type => 'array'},
+            {desc => q/Finalize PO's.  Array of 'acqpo' ID's/, type => 'array'},
         ],
         return => {desc => 'The invoice w/ entries and items attached', type => 'object', class => 'acqinv'}
     }
@@ -45,7 +46,9 @@ __PACKAGE__->register_method(
 
 
 sub build_invoice_impl {
-    my ($e, $invoice, $entries, $items, $do_commit) = @_;
+    my ($e, $invoice, $entries, $items, $do_commit, $finalize_pos) = @_;
+
+    $finalize_pos ||= [];
 
     if ($invoice->isnew) {
         $invoice->recv_method('PPR') unless $invoice->recv_method;
@@ -128,13 +131,12 @@ sub build_invoice_impl {
 
                         if ($U->is_true($item_type->blanket)) {
                             # Each payment toward a blanket charge results
-                            # in a new debit to track the payment and 
-                            # decreasing the (encumbered) amount on the 
-                            # origin po-item debit by the amount paid.
-
+                            # in a new debit to track the payment and a 
+                            # decrease in the original encumbrance by 
+                            # the amount paid on this invoice item
                             $debit->amount($debit->amount - $item->amount_paid);
                             $e->update_acq_fund_debit($debit) or return $e->die_event;
-                            $debit = undef;
+                            $debit = undef; # new debit created below
                         }
                     }
 
@@ -210,16 +212,15 @@ sub build_invoice_impl {
 
                 if ($U->is_true($item_type->blanket)) {
                     # modifying a payment against a blanket charge means
-                    # also modifying the amount encumbered on the source
-                    # debit from the blanket po_item to keep things balanced.
+                    # modifying the amount encumbered on the source debit
+                    # by the same (but opposite) amount.
 
                     my $po_debit = $e->retrieve_acq_fund_debit(
                         $item->po_item->fund_debit);
+
                     my $delta = $debit->amount - $item->amount_paid;
                     $po_debit->amount($po_debit->amount + $delta);
-
-                    $e->update_acq_fund_debit($po_debit) 
-                        or return $e->die_event;
+                    $e->update_acq_fund_debit($po_debit) or return $e->die_event;
                 }
 
 
@@ -239,6 +240,14 @@ sub build_invoice_impl {
         }
     }
 
+    for my $po_id (@$finalize_pos) {
+        my $po = $e->retrieve_acq_purchase_order($po_id) 
+            or return $e->die_event;
+        
+        my $evt = finalize_blanket_po($e, $po);
+        return $evt if $evt;
+    }
+
     $invoice = fetch_invoice_impl($e, $invoice->id);
     if ($do_commit) {
         $e->commit or return $e->die_event;
@@ -248,7 +257,7 @@ sub build_invoice_impl {
 }
 
 sub build_invoice_api {
-    my($self, $conn, $auth, $invoice, $entries, $items) = @_;
+    my($self, $conn, $auth, $invoice, $entries, $items, $finalize_pos) = @_;
 
     my $e = new_editor(xact => 1, authtoken=>$auth);
     return $e->die_event unless $e->checkauth;
@@ -265,7 +274,7 @@ sub build_invoice_api {
     return $e->die_event unless
         $e->allowed('CREATE_INVOICE', $invoice->receiver);
 
-    return build_invoice_impl($e, $invoice, $entries, $items, 1);
+    return build_invoice_impl($e, $invoice, $entries, $items, 1, $finalize_pos);
 }
 
 
@@ -748,4 +757,103 @@ sub print_html_invoice {
     undef;
 }
 
+__PACKAGE__->register_method(
+    method => 'finalize_blanket_po_api',
+    api_name    => 'open-ils.acq.purchase_order.blanket.finalize',
+    signature => {
+        desc => q/
+            1. Set encumbered amount to zero for all blanket po_item's
+            2. If the PO does not have any outstanding lineitems, mark
+               the PO as 'received'.
+        /,
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => q/PO ID/, type => 'number'}
+        ],
+        return => {desc => '1 on success, event on error'}
+    }
+);
+
+sub finalize_blanket_po_api {
+    my ($self, $client, $auth, $po_id) = @_;
+
+    my $e = new_editor(xact => 1, authtoken=>$auth);
+    return $e->die_event unless $e->checkauth;
+
+    my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
+
+    return $e->die_event unless
+        $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
+
+    my $evt = finalize_blanket_po($e, $po);
+    return $evt if $evt;
+
+    $e->commit;
+    return 1;
+}
+
+
+# 1. set any remaining blanket encumbrances to $0.
+# 2. mark the PO as received if there are no pending lineitems.
+sub finalize_blanket_po {
+    my ($e, $po) = @_;
+
+    my $po_id = $po->id;
+
+    # blanket po_items on this PO
+    my $blanket_items = $e->json_query({
+        select => {acqpoi => ['id']},
+        from => {acqpoi => {aiit => {}}},
+        where => {
+            '+aiit' => {blanket => 't'},
+            '+acqpoi' => {purchase_order => $po_id}
+        }
+    });
+
+    for my $item_id (map { $_->{id} } @$blanket_items) {
+
+        my $item = $e->retrieve_acq_po_item([
+            $item_id, {
+                flesh => 1,
+                flesh_fields => {acqpoi => ['fund_debit']}
+            }
+        ]); 
+
+        my $debit = $item->fund_debit or next;
+
+        next unless $U->is_true($debit->encumbrance);
+
+        $debit->amount(0);
+        $debit->encumbrance('f');
+        $e->update_acq_fund_debit($debit) or return $e->die_event;
+    }
+
+    # Number of pending lineitems on this PO. 
+    # If there are any, we don't mark 'received'
+    my $li_count = $e->json_query({
+        select => {jub => [{column => 'id', transform => 'count'}]},
+        from => 'jub',
+        where => {
+            '+jub' => {
+                purchase_order => $po_id,
+                state => 'on-order'
+            }
+        }
+    })->[0];
+    
+    if ($li_count->{count} > 0) {
+        $logger->info("skipping 'received' state change for po $po_id ".
+            "during finalization, because PO has pending lineitems");
+        return undef;
+    }
+
+    $po->state('received');
+    $po->edit_time('now');
+    $po->editor($e->requestor->id);
+
+    $e->update_acq_purchase_order($po) or return $e->die_event;
+
+    return undef;
+}
+
 1;
index 678eec5..69b09bc 100644 (file)
         </div>
     </div>
 
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+        <div class='hidden' id='oils-acq-final-invoice-pane'>
+            <table>
+                <tbody id='acq-final-invoice-tbody'>
+                    <tr id='acq-final-invoice-row'>
+                        <td>[% l('Final invoice for Blanket order?') %]</td>
+                        <td><div name='po-label'></td>
+                        <td><div name='checkbox'></div></td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
     <!--
     <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
     -->
index 1887983..1dd1ccb 100644 (file)
@@ -284,3 +284,14 @@ span[name="cancel_reason"] { text-decoration: underline; font-weight: bold; }
     font-weight: bold;
     color: red;
 }
+
+#oils-acq-final-invoice-pane {
+    margin-top: 10px;
+    margin-bottom: 10px;
+}
+
+#acq-final-invoice-tbody td {
+    padding: 6px;
+    border: 1px solid #AAA;
+}
+
index 0248ad6..9eaeabb 100644 (file)
@@ -68,6 +68,7 @@
     "INVOICE_CONFIRM_PRORATE" : "Prorate charges?\n\nAny subsequent changes to the invoice that would affect prorated amounts should be resolved manually.",
     "INVOICE_EXTRA_COPIES" : "You are attempting to invoice <b>${0}</b> more copies than originally ordered.  <br/><br/>To add these items to the original order, select a fund and choose 'Add New Items' below.  <br/>After saving the invoice, you may finish editing and importing the new copies from the lineitem details page.",
     "INVOICE_ITEM_PO_DETAILS" : "<b>${0}</b><br/><a href='${1}/acq/po/view/${2}'>PO #${3} ${4}</a><br/>Total Estimated Cost: $${5}",
+    "INVOICE_ITEM_PO_LABEL" : "<a href='${0}/acq/po/view/${1}'>PO #${2} ${3}</a><br/>Total Estimated Cost: $${4}",
     "UNNAMED" : "Unnamed",
     "NO_FIND_INVOICE" : "Could not find that invoice.\nNote that the Invoice # field is case-sensitive.",
     "LI_BATCH_UPDATE": "Line item batch update",
index 313e4d7..4fbc16a 100644 (file)
@@ -43,6 +43,7 @@ var focusLineitem;
 var searchInitDone = false;
 var termManager;
 var resultManager;
+var finalizePos = [];
 
 function nodeByName(name, context) {
     return dojo.query('[name='+name+']', context)[0];
@@ -573,6 +574,37 @@ function registerWidget(obj, field, widget, callback) {
     return widget;
 }
 
+var finalInvTbody, finalInvRow;
+var finalInvPoSeen = {};
+function addMarkFinalPO(item, po_item, po_label) {
+
+    if (finalInvPoSeen[po_item.purchase_order()]) return;
+    finalInvPoSeen[po_item.purchase_order()] = true;
+
+    openils.Util.show(dojo.byId('oils-acq-final-invoice-pane'));
+
+    if (!finalInvTbody) {
+        finalInvTbody = dojo.byId('acq-final-invoice-tbody');
+        finalInvRow = finalInvTbody.removeChild(
+            dojo.byId('acq-final-invoice-row'));
+    }
+
+    var row = finalInvRow.cloneNode(true);
+    nodeByName('po-label', row).innerHTML = po_label;
+    var cbox = new dijit.form.CheckBox({}, nodeByName('checkbox', row));
+
+    dojo.connect(cbox, 'onChange', function(set) {
+        if (set) { // add to finalize list
+            finalizePos.push(Number(po_item.purchase_order()));
+        } else { // remove from finalize list
+            finalizePos = finalizePos.filter(
+                function(id) {return id != po_item.purchase_order()});
+        }
+    });
+
+    finalInvTbody.appendChild(row);
+}
+
 function addInvoiceItem(item) {
     itemTbody = dojo.byId('acq-invoice-item-tbody');
     if(itemTemplate == null) {
@@ -667,6 +699,34 @@ function addInvoiceItem(item) {
             ]
         );
 
+        if (openils.Util.isTrue(itemType.blanket()) 
+                && po.state() != 'received') {
+
+            fieldmapper.standardRequest(
+                ['open-ils.acq', 
+                    'open-ils.acq.purchase_order.retrieve.authoritative'],
+                {   async: true,
+                    params: [openils.User.authtoken, po.id(), {
+                        "flesh_price_summary": true
+                    }],
+                    oncomplete: function(r) {
+                        // update the global PO instead of replacing it, since other 
+                        // code outside our control may be referencing it.
+                        var po2 = openils.Util.readResponse(r);
+
+                        var po_label = dojo.string.substitute(
+                            localeStrings.INVOICE_ITEM_PO_LABEL,
+                            [ oilsBasePath, po2.id(), po2.name(), 
+                              orderDate, po2.amount_estimated().toFixed(2)
+                            ]
+                        );
+
+                        addMarkFinalPO(item, po_item, po_label);
+                    }
+                }
+            );
+        }
+
     } else {
 
         registerWidget(
@@ -1059,7 +1119,8 @@ function saveChangesPartTwo(args) {
     fieldmapper.standardRequest(
         ['open-ils.acq', 'open-ils.acq.invoice.update'],
         {
-            params : [openils.User.authtoken, invoice, updateEntries, updateItems],
+            params : [openils.User.authtoken,
+                invoice, updateEntries, updateItems, finalizePos],
             oncomplete : function(r) {
                 progressDialog.hide();
                 var invoice = openils.Util.readResponse(r);