From a58aeba14ecb30e36acee531efc3599648dd3d4d Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 10 Apr 2015 12:30:22 -0400 Subject: [PATCH] LP#1440114 Blanket order PO "finalize" 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 Signed-off-by: Kathy Lussier --- .../lib/OpenILS/Application/Acq/Invoice.pm | 134 ++++++++++++++++-- Open-ILS/src/templates/acq/invoice/view.tt2 | 14 ++ Open-ILS/web/css/skin/default/acq.css | 11 ++ Open-ILS/web/js/dojo/openils/acq/nls/acq.js | 1 + .../web/js/ui/default/acq/invoice/view.js | 63 +++++++- 5 files changed, 209 insertions(+), 14 deletions(-) diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm index aa3bf49c7f..3980e6215c 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm @@ -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; diff --git a/Open-ILS/src/templates/acq/invoice/view.tt2 b/Open-ILS/src/templates/acq/invoice/view.tt2 index 678eec5a9a..69b09bcc67 100644 --- a/Open-ILS/src/templates/acq/invoice/view.tt2 +++ b/Open-ILS/src/templates/acq/invoice/view.tt2 @@ -45,6 +45,20 @@ +
+ +
+ diff --git a/Open-ILS/web/css/skin/default/acq.css b/Open-ILS/web/css/skin/default/acq.css index 1887983f5b..1dd1ccb70c 100644 --- a/Open-ILS/web/css/skin/default/acq.css +++ b/Open-ILS/web/css/skin/default/acq.css @@ -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; +} + diff --git a/Open-ILS/web/js/dojo/openils/acq/nls/acq.js b/Open-ILS/web/js/dojo/openils/acq/nls/acq.js index 0248ad6ef3..9eaeabbc4a 100644 --- a/Open-ILS/web/js/dojo/openils/acq/nls/acq.js +++ b/Open-ILS/web/js/dojo/openils/acq/nls/acq.js @@ -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 ${0} more copies than originally ordered.

To add these items to the original order, select a fund and choose 'Add New Items' below.
After saving the invoice, you may finish editing and importing the new copies from the lineitem details page.", "INVOICE_ITEM_PO_DETAILS" : "${0}
PO #${3} ${4}
Total Estimated Cost: $${5}", + "INVOICE_ITEM_PO_LABEL" : "PO #${2} ${3}
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", diff --git a/Open-ILS/web/js/ui/default/acq/invoice/view.js b/Open-ILS/web/js/ui/default/acq/invoice/view.js index 313e4d7185..4fbc16a846 100644 --- a/Open-ILS/web/js/ui/default/acq/invoice/view.js +++ b/Open-ILS/web/js/ui/default/acq/invoice/view.js @@ -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); -- 2.43.2