From: Bill Erickson Date: Tue, 10 Jul 2012 15:07:53 +0000 (-0400) Subject: ACQ invoice inline lineitem search and add X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=d7177aa1a4ad46d00dbea4489e3388d0ba1cc772;p=evergreen%2Fpines.git ACQ invoice inline lineitem search and add The Invoice UI is how composed of two tabs, the main invoice tab and a new Search tab. The search tab consists of a subset of the Acquisitions unified search interface. The goal is to allow users to search for lineitems to invoice. Search results may be added directly to the growing invoice. A number of small usability features are included. Features ~~~~~~~~ * Option (default) to limit searches to invoiceable items. ** These are lineitems that are not cancelled, have at least one invoiceable copy, linked to a PO whose provider matches that of the current invoice, and are not already linked to the current invoice. * Search defaults to last-run search (on workstation). * New Lineitem Detail filter options * Sort searches by lineitem number (default) and title. * There is a new Expected Cost field which includes both the total invoiced cost plus the anticipated cost of lineitems as they are added. * New Price per Copy field * Lineitem count field * Show / Hide Invoice details button. Details are displayed by default, but hidden when the user enters the search tab. From there it remains hidden until manually shown (or a new invoice is opened). * A new "Save & Clear" button which saves the current invoice then clears the invoice display to create a new invoice. * Provider, shipper, and receiver fields are auto-populated from the first-added invoice data (when not already set). * Totals are now read-only, since they are derived from existing data (and are informational only). Signed-off-by: Bill Erickson Signed-off-by: Lebbeous Fogle-Weekley --- diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm index cbf4c73c5c..da594a2355 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm @@ -229,7 +229,7 @@ sub prepare_terms { my $conj = $is_and ? "-and" : "-or"; my $outer_clause = {}; - foreach my $class (qw/acqpo acqpl acqinv jub/) { + foreach my $class (qw/acqpo acqpl acqinv jub acqlid acqlisum acqlisumi/) { next if not exists $terms->{$class}; $outer_clause->{$conj} = [] unless $outer_clause->{$conj}; @@ -243,7 +243,12 @@ sub prepare_terms { } elsif ($between and could_be_range($v)) { $term_clause = {$k => {"between" => $v}}; } elsif (check_1d_max($v)) { - $v = castdate($v, $gte, $lte) if $castdate; + if ($castdate) { + $v = castdate($v, $gte, $lte) if $castdate; + } elsif ($gte or $lte) { + my $op = $gte ? '>=' : '<='; + $v = {$op => $v}; + } $term_clause = {$k => $v}; } else { next; @@ -339,6 +344,11 @@ sub build_from_clause_and_joins { } else { $graft_map{$class} = $query->{from}{$core}{$class} ||= {}; $graft_map{$class}{type} = $join_type; + + # without this, the SQL attempts to join on + # jub.order_summary, which is a virtual field. + $graft_map{$class}{field} = 'lineitem' + if $class eq 'acqlisum' or $class eq 'acqlisumi'; } } @@ -415,6 +425,7 @@ sub unified_search { $hint => [{"column" => "id", "transform" => "distinct"}] }; + my $attr_from_filter; if ($options->{"order_by"}) { # What's the point of this block? When using ORDER BY in conjuction # with SELECT DISTINCT, the fields present in ORDER BY have to also @@ -432,9 +443,30 @@ sub unified_search { q/order_by clause must be of the long form, like: "order_by": [{"class": "foo", "field": "bar", "direction": "asc"}]/ ); + } else { + + # we can't combine distinct(id) with another select column, + # since the non-distinct column may arbitrarily (via hash keys) + # sort to the front of the final SQL, which PG will complain about. + $select_clause = { $hint => ["id"] }; $select_clause->{$class} ||= []; - push @{$select_clause->{$class}}, $field; + push @{$select_clause->{$class}}, + {column => $field, transform => 'first', aggregate => 1}; + + # when sorting by LI attr values, we have to limit + # to a specific type of attr value to sort on. + if ($class eq 'acqlia') { + $attr_from_filter = { + "fkey" => "id", + "filter" => { + "attr_type" => "lineitem_marc_attr_definition", + "attr_name" => $options->{"order_by_attr"} || "title" + }, + "type" => "left", + "field" =>"lineitem" + }; + } } } @@ -469,6 +501,14 @@ q/order_by clause must be of the long form, like: return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms"); } + + # if ordering by acqlia, insert the from clause + # filter to limit to one type of attr. + if ($attr_from_filter) { + $query->{from}->{jub} = {} unless $query->{from}->{jub}; + $query->{from}->{jub}->{acqlia} = $attr_from_filter; + } + my $results = $e->json_query($query) or return $e->die_event; my @id_list = map { $_->{"id"} } (grep { $_->{"id"} } @$results); diff --git a/Open-ILS/src/templates/acq/invoice/view.tt2 b/Open-ILS/src/templates/acq/invoice/view.tt2 index 7e4d48931b..6ed29f1596 100644 --- a/Open-ILS/src/templates/acq/invoice/view.tt2 +++ b/Open-ILS/src/templates/acq/invoice/view.tt2 @@ -1,122 +1,307 @@ [% WRAPPER 'base.tt2' %] [% ctx.page_title = 'Invoicing' %] -
Invoice
- +
-
-
+ + + + + + + +
[% l("Lineitems: " ) %]0
[% l("Expected Cost: " ) %]0.00
+
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Direct Charges, Taxes, Fees, etc.

-
Charge TypeFundTitle/DescriptionBilledPaidDelete
Delete
- Add Charge... -
-

-
- TotalTotalBalance
- - - - -
+
+ +
+
+
+ + +
+
+
+ +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Direct Charges, Taxes, Fees, etc.

+
Charge TypeFundTitle/DescriptionBilled + PaidDelete
+
Delete
+ Add Charge... +
+

+
+ Total + TotalBalance
+ + + + + +
+
+
+ +
+ + + + + + +
+
+
+
@@ -132,6 +317,12 @@
+ + + [% END %] diff --git a/Open-ILS/web/css/skin/default/acq.css b/Open-ILS/web/css/skin/default/acq.css index 6d03933675..07fccb8865 100644 --- a/Open-ILS/web/css/skin/default/acq.css +++ b/Open-ILS/web/css/skin/default/acq.css @@ -211,7 +211,7 @@ span[name="notes_alert_flag"] {color: #c00;font-weight: bold;font-size: 110%;mar .acq-inoice-item-extra-info { padding-left: 10px; } .acq-inoice-item-info { font-weight: bold; } .acq-invoice-row td { border-bottom: 1px solid #e0e0e0; } -.acq-invoice-invalid-amount input { color: red; font-weight: bold; } +.acq-invoice-invalid-amount { color: red; font-weight: bold; } .acq-link-invoice-dialog td,.acq-link-invoice-dialog th {padding-top: 10px;} .acq-invoice-paid-col {background : #E0E0E0; text-align: center;} .acq-invoice-center-col { text-align: center; } diff --git a/Open-ILS/web/js/ui/default/acq/invoice/common.js b/Open-ILS/web/js/ui/default/acq/invoice/common.js index 03fade4c8e..0980fca978 100644 --- a/Open-ILS/web/js/ui/default/acq/invoice/common.js +++ b/Open-ILS/web/js/ui/default/acq/invoice/common.js @@ -4,18 +4,26 @@ dojo.require('openils.widget.EditPane'); function drawInvoicePane(parentNode, inv, args) { args = args || {}; + var pane; var override = {}; if(!inv) { override = { recv_date : {widgetValue : dojo.date.stamp.toISOString(new Date())}, - receiver : {widgetValue : openils.User.user.ws_ou()}, + //receiver : {widgetValue : openils.User.user.ws_ou()}, recv_method : {widgetValue : 'PPR'} }; } dojo.mixin(override, { - provider : { dijitArgs : { store_options : { base_filter : { active :"t" } } } }, + provider : { + dijitArgs : { + store_options : { base_filter : { active :"t" } }, + onChange : function(val) { + pane.setFieldValue('shipper', val); + } + } + }, shipper : { dijitArgs : { store_options : { base_filter : { active :"t" } } } } }); @@ -23,7 +31,19 @@ function drawInvoicePane(parentNode, inv, args) { override[field] = {widgetValue : args[field]}; } - var pane = new openils.widget.EditPane({ + // push the name of the invoice into the name display field after update + override.inv_ident = dojo.mixin( + override.inv_ident, + {dijitArgs : {onChange : + function(newVal) { + if (dojo.byId('acq-invoice-summary-name')) + dojo.byId('acq-invoice-summary-name').innerHTML = newVal; + } + }} + ); + + + pane = new openils.widget.EditPane({ fmObject : inv, paneStackCount : 2, fmClass : 'acqinv', 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 2e8f499eeb..4f1fa607c4 100644 --- a/Open-ILS/web/js/ui/default/acq/invoice/view.js +++ b/Open-ILS/web/js/ui/default/acq/invoice/view.js @@ -1,6 +1,8 @@ dojo.require('dojo.date.locale'); dojo.require('dojo.date.stamp'); +dojo.require('dojo.cookie'); dojo.require('dijit.form.CheckBox'); +dojo.require('dijit.form.Button'); dojo.require('dijit.form.CurrencyTextBox'); dojo.require('dijit.form.NumberTextBox'); dojo.require('openils.User'); @@ -36,6 +38,9 @@ var extraCopies = {}; var extraCopiesFund; var widgetRegistry = {acqie : {}, acqii : {}}; var focusLineitem; +var searchInitDone = false; +var termManager; +var resultManager; function nodeByName(name, context) { return dojo.query('[name='+name+']', context)[0]; @@ -53,12 +58,30 @@ function init() { focusLineitem = new openils.CGI().param('focus_li'); + totalInvoicedBox = dojo.byId('acq-total-invoiced-box'); + totalPaidBox = dojo.byId('acq-total-paid-box'); + balanceOwedBox = dojo.byId('acq-total-balance-box'); + itemTypes = pcrud.retrieveAll('aiit'); + dojo.byId('acq-invoice-summary-toggle-off').onclick = function() { + openils.Util.hide(dojo.byId('acq-invoice-summary')); + openils.Util.show(dojo.byId('acq-invoice-summary-small')); + }; + + dojo.byId('acq-invoice-summary-toggle-on').onclick = function() { + openils.Util.show(dojo.byId('acq-invoice-summary')); + openils.Util.hide(dojo.byId('acq-invoice-summary-small')); + } + if(cgi.param('create')) { renderInvoice(); + // show summary info by default for new invoices + dojo.byId('acq-invoice-summary-toggle-on').onclick(); + } else { + dojo.byId('acq-invoice-summary-toggle-off').onclick(); fieldmapper.standardRequest( ['open-ils.acq', 'open-ils.acq.invoice.retrieve.authoritative'], { @@ -117,16 +140,20 @@ function renderInvoice() { ); } + // display items and entries in ID order + // which effectively equates to add order. + function idsort(a, b) { return a.id() < b.id() ? -1 : 1 } + if(invoice) { dojo.forEach( - invoice.items(), + invoice.items().sort(idsort), function(item) { addInvoiceItem(item); } ); dojo.forEach( - invoice.entries(), + invoice.entries().sort(idsort), function(entry) { addInvoiceEntry(entry); } @@ -137,10 +164,10 @@ function renderInvoice() { if(attachPo.length) doAttachPo(0); } -function doAttachLi() { +function doAttachLi(skipInit) { //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; - if(cgi.param('create')) { + if(cgi.param('create') && !skipInit) { // use the first LI in the list to determine the default provider fieldmapper.standardRequest( @@ -223,6 +250,172 @@ function doAttachPo(idx) { ); } +function performSearch(pageDir) { + clearSearchResTable(); + var searchObject = termManager.buildSearchObject(); + dojo.cookie('invs', base64Encode(searchObject)); + dojo.cookie('invc', dojo.byId("acq-unified-conjunction").getValue()); + + if (pageDir == 0) { // new search + resultsLoader.displayOffset = 0; + } else { + resultsLoader.displayOffset += pageDir * resultsLoader.displayLimit; + } + + if (resultsLoader.displayOffset == 0) { + openils.Util.hide('acq-inv-search-prev'); + } else { + openils.Util.show('acq-inv-search-prev', 'inline'); + } + + if (dojo.byId('acq-invoice-search-limit-invoiceable').checked) { + if (!searchObject.jub) + searchObject.jub = []; + + // exclude lineitems that are "cancelled" (sidebar: 'Mericans spell it 'canceled') + searchObject.jub.push({state : 'cancelled', '__not' : true}); + + // exclude lineitems already linked to this invoice + if (invoice && invoice.id() > 0) { + if (!searchObject.acqinv) + searchObject.acqinv = []; + searchObject.acqinv.push({id : invoice.id(), '__not' : true}); + } + + // limit to lineitems that have invoiceable copies + searchObject.acqlisumi = [{item_count : 1, '_gte' : true}]; + + // limit to provider if a provider is selected + var provider = invoicePane.getFieldValue('provider'); + if (provider) { + if (!searchObject.jub.filter(function(i) { return i.provider != null }).length) + searchObject.jub.push({provider : provider}); + } + } + + if (dojo.byId('acq-invoice-search-sort-title').checked) { + uriManager.order_by = + [ {"class": "acqlia", "field":"attr_value", "transform":"first"} ]; + } + + resultsLoader.lastSearch = searchObject; + resultManager.go(searchObject) + console.log('Lineitem Search: ' + js2JSON(searchObject)); + focusLastSearchInput(); +} + + +function renderUnifiedSearch() { + + if (!searchInitDone) { + + searchInitDone = true; + termManager = new TermManager(); + resultManager = new ResultManager(); + resultsLoader = new searchResultsLoader(); + uriManager = new URIManager(); + + // define custom lineitem result handler + resultManager.result_types = { + "lineitem": { + "search_options": { "id_list": true }, + "revealer": function() { }, + "finisher": function() { + resultsLoader.batch_length = resultManager.count_results; + }, + "adder": function(li) { + resultsLoader.addLineitem(li); + }, + "interface": resultsLoader + }, + "no_results": { + "revealer": function() { } + } + }; + + var searchObject = dojo.cookie('invs'); + console.log('loaded ' + searchObject); + if (searchObject) { + // if there is a search object cookie, populate the search form + termManager.reflect(base64Decode(searchObject)); + dojo.byId("acq-unified-conjunction").setValue(dojo.cookie('invc')); + } else { + console.log('adding row'); + termManager.addRow(); + } + } + + dojo.addClass(dojo.byId('oils-acq-invoice-table'), 'hidden'); + dojo.removeClass(dojo.byId('oils-acq-invoice-search'), 'hidden'); + focusLastSearchInput(); +} + +function focusLastSearchInput() { + // TODO: see about making this better and moving it into search/unified.js + var wnodes = dojo.query('[name=widget]'); + var inputNode = wnodes.item(wnodes.length - 1).firstChild; + if (inputNode) { + try { + inputNode.select(); + } catch(E) { + inputNode.focus(); + } + } +} + +var resultsTbody, resultsRow; +function searchResultsLoader() { + this.displayOffset = 0; + this.displayLimit = 10; + + if (!resultsTbody) { + resultsTbody = dojo.byId('acq-invoice-search-results-tbody'); + resultsRow = resultsTbody.removeChild(dojo.byId('acq-invoice-search-results-tr')); + } + + this.addLineitem = function(li_id) { + console.log('Adding search result lineitem ' + li_id); + var row = resultsRow.cloneNode(true); + resultsTbody.appendChild(row); + var checkbox = dojo.query('[name=search-results-checkbox]', row)[0]; + checkbox.setAttribute('lineitem', li_id); + + // this lineitem is already part of the invoice + if (dojo.query('[entry_lineitem_row=' + li_id + ']')[0]) { + checkbox.disabled = true; + dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced'); + } + + openils.acq.Lineitem.fetchAndRender( + li_id, {}, + function(li, html) { + dojo.query('[name=search-results-content-div]', row)[0].innerHTML = html; + } + ); + } +} + +function addSelectedToInvoice() { + var inputs = dojo.query('[name=search-results-checkbox]'); + attachLi = []; + dojo.forEach(inputs, + function(checkbox) { + if (checkbox.checked) { + attachLi.push(checkbox.getAttribute('lineitem')); + checkbox.disabled = true; + checkbox.checked = false; + dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced'); + } + } + ); + doAttachLi(true); +} + +function clearSearchResTable() { + while (resultsTbody.childNodes[0]) + resultsTbody.removeChild(resultsTbody.childNodes[0]); +} + function updateTotalCost() { var totalCost = 0; @@ -232,7 +425,7 @@ function updateTotalCost() { for(var id in widgetRegistry.acqie) if(!widgetRegistry.acqie[id]._object.isdeleted()) totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue()); - totalInvoicedBox.attr('value', totalCost); + totalInvoicedBox.innerHTML = totalCost.toFixed(2); totalPaid = 0; for(var id in widgetRegistry.acqii) @@ -241,27 +434,27 @@ function updateTotalCost() { for(var id in widgetRegistry.acqie) if(!widgetRegistry.acqie[id]._object.isdeleted()) totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue()); - totalPaidBox.attr('value', totalPaid); + totalPaidBox.innerHTML = totalPaid.toFixed(2); var buttonsDisabled = false; if(totalPaid > totalCost || totalPaid < 0) { - openils.Util.addCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount'); + openils.Util.addCSSClass(totalPaidBox, 'acq-invoice-invalid-amount'); invoiceSaveButton.attr('disabled', true); invoiceProrateButton.attr('disabled', true); buttonsDisabled = true; } else { - openils.Util.removeCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount'); + openils.Util.removeCSSClass(totalPaidBox, 'acq-invoice-invalid-amount'); invoiceSaveButton.attr('disabled', false); invoiceProrateButton.attr('disabled', false); } if(totalCost < 0) { - openils.Util.addCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount'); + openils.Util.addCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount'); invoiceSaveButton.attr('disabled', true); invoiceProrateButton.attr('disabled', true); } else { - openils.Util.removeCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount'); + openils.Util.removeCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount'); if(!buttonsDisabled) { invoiceSaveButton.attr('disabled', false); invoiceProrateButton.attr('disabled', false); @@ -274,7 +467,9 @@ function updateTotalCost() { invoiceCloseButton.attr('disabled', true); } - balanceOwedBox.attr('value', (totalCost - totalPaid)); + balanceOwedBox.innerHTML = (totalCost - totalPaid).toFixed(2); + + updateExpectedCost(); } @@ -316,6 +511,7 @@ function addInvoiceItem(item) { } else if(field == 'cost_billed' || field == 'amount_paid') { args = {required : true, style : 'width: 8em'}; } + registerWidget( item, field, @@ -481,12 +677,39 @@ function focusLi() { } +// expected cost is totalCostInvoiced + totalCostNotYetInvoiced +function updateExpectedCost() { + + var cost = Number(totalInvoicedBox.innerHTML || 0); + + // for any LI's that are not yet billed (i.e. filled in) + // use the total expected cost for that lineitem. + for(var id in widgetRegistry.acqie) { + var entry = widgetRegistry.acqie[id]._object; + if(!entry.isdeleted()) { + if (Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue()) == 0) { + var li = entry.lineitem(); + cost += + Number(li.order_summary().estimated_amount()) - + Number(li.order_summary().paid_amount()); + } + } + } + + dojo.byId('acq-invoice-summary-cost').innerHTML = cost.toFixed(2); +} + +var invoicEntryWidgets = {}; function addInvoiceEntry(entry) { + console.log('Adding new entry for lineitem ' + entry.lineitem()); openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden'); openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden'); openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden'); + dojo.byId('acq-invoice-summary-count').innerHTML = + Number(dojo.byId('acq-invoice-summary-count').innerHTML) + 1; + entryTbody = dojo.byId('acq-invoice-entry-tbody'); if(entryTemplate == null) { entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template')); @@ -498,6 +721,7 @@ function addInvoiceEntry(entry) { var row = entryTemplate.cloneNode(true); row.setAttribute('lineitem', entry.lineitem()); + row.setAttribute('entry_lineitem_row', entry.lineitem()); openils.acq.Lineitem.fetchAndRender( entry.lineitem(), {}, @@ -511,6 +735,14 @@ function addInvoiceEntry(entry) { updateReceiveLink(li); + // set some default values if otherwise unset + if (!invoicePane.getFieldValue('receiver')) { + invoicePane.setFieldValue('receiver', li.purchase_order().ordering_agency()); + } + if (!invoicePane.getFieldValue('provider')) { + invoicePane.setFieldValue('provider', li.purchase_order().provider()); + } + dojo.forEach( ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'], function(field) { @@ -536,6 +768,7 @@ function addInvoiceEntry(entry) { parentNode : nodeByName(field, row) }), function(w) { + if(field == 'phys_item_count') { dojo.connect(w, 'onChange', function() { @@ -548,12 +781,20 @@ function addInvoiceEntry(entry) { } } ) - } - } + } // if + + if(field == 'inv_item_count' || field == 'cost_billed') { + setPerCopyPrice(row, entry); + // update the per-copy count as invoice count and cost billed change + dojo.connect(w, 'onChange', function() { setPerCopyPrice(row, entry) } ); + } + + } // func ); } ); + updateTotalCost(); if (focusLineitem == li.id()) focusLi(); } @@ -586,7 +827,22 @@ function addInvoiceEntry(entry) { } entryTbody.appendChild(row); - updateTotalCost(); +} + +function setPerCopyPrice(row, entry) { + var inv_w = widgetRegistry.acqie[entry.id()].inv_item_count; + var bill_w = widgetRegistry.acqie[entry.id()].cost_billed; + + if (inv_w && bill_w) { + var invoiced = Number(inv_w.getFormattedValue()); + var billed = Number(bill_w.getFormattedValue()); + console.log(invoiced + ' : ' + billed); + if (invoiced > 0) { + nodeByName('amount_paid_per_copy', row).innerHTML = (billed / invoiced).toFixed(2); + } else { + nodeByName('amount_paid_per_copy', row).innerHTML = '0.00'; + } + } } function liMarcAttr(lineitem, name) { @@ -601,12 +857,9 @@ function liMarcAttr(lineitem, name) { return (attr) ? attr.attr_value() : ''; } -function saveChanges(doProrate, doClose, doReopen) { - createExtraCopies( - function() { - saveChangesPartTwo(doProrate, doClose, doReopen); - } - ); +function saveChanges(args) { + args = args || {}; + createExtraCopies(function() { saveChangesPartTwo(args); }); } // Define a helper function to 'unflesh' sub-objects from an fmclass object. @@ -621,10 +874,10 @@ function unflesh() { }); } -function saveChangesPartTwo(doProrate, doClose, doReopen) { - +function saveChangesPartTwo(args) { + args = args || {}; - if(doReopen) { + if(args.reopen) { invoice.complete('f'); } else { @@ -643,7 +896,7 @@ function saveChangesPartTwo(doProrate, doClose, doReopen) { return; } - if(doClose) + if(args.close) invoice.complete('t'); @@ -693,9 +946,13 @@ function saveChangesPartTwo(doProrate, doClose, doReopen) { progressDialog.hide(); var invoice = openils.Util.readResponse(r); if(invoice) { - if(doProrate) + if(args.prorate) return prorateInvoice(invoice); - location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id(); + if (args.clear) { + location.href = oilsBasePath + '/acq/invoice/view?create=1'; + } else { + location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id(); + } } } } diff --git a/Open-ILS/web/js/ui/default/acq/search/unified.js b/Open-ILS/web/js/ui/default/acq/search/unified.js index b7f0137836..2c1eff01bc 100644 --- a/Open-ILS/web/js/ui/default/acq/search/unified.js +++ b/Open-ILS/web/js/ui/default/acq/search/unified.js @@ -4,6 +4,7 @@ dojo.require("openils.widget.AutoGrid"); dojo.require("openils.widget.AutoWidget"); dojo.require("openils.widget.XULTermLoader"); dojo.require("openils.PermaCrud"); +dojo.require('dijit.layout.TabContainer'); if (!localeStrings) { /* we can do this because javascript doesn't have block scope */ @@ -251,7 +252,7 @@ function TermManager() { }; this.terms = {}; - ["jub", "acqpl", "acqpo", "acqinv"].forEach( + ["jub", "acqpl", "acqpo", "acqinv", "acqlid"].forEach( function(hint) { var o = {}; o.__label = fieldmapper.IDL.fmclasses[hint].label; @@ -621,84 +622,91 @@ function ResultManager(liPager, poGrid, plGrid, invGrid) { this.plCache = {}; this.invCache = {}; - this.result_types = { - "lineitem": { - "search_options": { - "flesh_attrs": true, - "flesh_cancel_reason": true, - "flesh_notes": true - }, - "revealer": function() { - self.liPager.show(); - progressDialog.show(true); - }, - "finisher": function() { - self.liPager.batch_length = self.count_results; - self.liPager.relabelControls(); - self.liPager.enableControls(true); - progressDialog.hide(); - }, - "adder": function(li) { - self.liPager.liTable.addLineitem(li); - }, - "interface": self.liPager - }, - "purchase_order": { - "search_options": { - "no_flesh_cancel_reason": true - }, - "revealer": function() { - self.poGrid.resetStore(); - self.poGrid.showLoadProgressIndicator(); - self.poCache = {}; - }, - "finisher": function() { - self.poGrid.hideLoadProgressIndicator(); - }, - "adder": function(po) { - self.poCache[po.id()] = po; - self.poGrid.store.newItem(acqpo.toStoreItem(po)); - }, - "interface": self.poGrid - }, - "picklist": { - "search_options": { - "flesh_lineitem_count": true, - "flesh_owner": true - }, - "revealer": function() { - self.plGrid.resetStore(); - self.plGrid.showLoadProgressIndicator(); - self.plCache = {}; - }, - "finisher": function() { - self.plGrid.hideLoadProgressIndicator(); - }, - "adder": function(pl) { - self.plCache[pl.id()] = pl; - self.plGrid.store.newItem(acqpl.toStoreItem(pl)); - }, - "interface": self.plGrid - }, - "invoice": { - "search_options": { - "no_flesh_misc": true + if (window.unifiedSearchExternalMode) { + + // external user will define result types and handlers + + } else { + + this.result_types = { + "lineitem": { + "search_options": { + "flesh_attrs": true, + "flesh_cancel_reason": true, + "flesh_notes": true + }, + "revealer": function() { + self.liPager.show(); + progressDialog.show(true); + }, + "finisher": function() { + self.liPager.batch_length = self.count_results; + self.liPager.relabelControls(); + self.liPager.enableControls(true); + progressDialog.hide(); + }, + "adder": function(li) { + self.liPager.liTable.addLineitem(li); + }, + "interface": self.liPager }, - "finisher": function() { - self.invGrid.hideLoadProgressIndicator(); + "purchase_order": { + "search_options": { + "no_flesh_cancel_reason": true + }, + "revealer": function() { + self.poGrid.resetStore(); + self.poGrid.showLoadProgressIndicator(); + self.poCache = {}; + }, + "finisher": function() { + self.poGrid.hideLoadProgressIndicator(); + }, + "adder": function(po) { + self.poCache[po.id()] = po; + self.poGrid.store.newItem(acqpo.toStoreItem(po)); + }, + "interface": self.poGrid }, - "revealer": function() { - self.invGrid.resetStore(); - self.invCache = {}; + "picklist": { + "search_options": { + "flesh_lineitem_count": true, + "flesh_owner": true + }, + "revealer": function() { + self.plGrid.resetStore(); + self.plGrid.showLoadProgressIndicator(); + self.plCache = {}; + }, + "finisher": function() { + self.plGrid.hideLoadProgressIndicator(); + }, + "adder": function(pl) { + self.plCache[pl.id()] = pl; + self.plGrid.store.newItem(acqpl.toStoreItem(pl)); + }, + "interface": self.plGrid }, - "adder": function(inv) { - self.invCache[inv.id()] = inv; - self.invGrid.store.newItem(acqinv.toStoreItem(inv)); + "invoice": { + "search_options": { + "no_flesh_misc": true + }, + "finisher": function() { + self.invGrid.hideLoadProgressIndicator(); + }, + "revealer": function() { + self.invGrid.resetStore(); + self.invCache = {}; + }, + "adder": function(inv) { + self.invCache[inv.id()] = inv; + self.invGrid.store.newItem(acqinv.toStoreItem(inv)); + }, + "interface": self.invGrid }, - "interface": self.invGrid - }, - "no_results": { - "revealer": function() { alert(localeStrings.NO_RESULTS); } + "no_results": { + "revealer": function() { alert(localeStrings.NO_RESULTS); } + } } }; @@ -788,10 +796,23 @@ function ResultManager(liPager, poGrid, plGrid, invGrid) { }; this.go = function(search_object) { + + if (window.unifiedSearchExternalMode) { + // assume for now that external mode implies inline results display + + uriManager = uriManager || new URIManager(); + uriManager.search_object = search_object; + uriManager.result_type = dojo.byId("acq-unified-result-type").getValue(); + uriManager.conjunction = dojo.byId("acq-unified-conjunction").getValue(); + this.search(uriManager, termManager); + + } else { + location.href = oilsBasePath + "/acq/search/unified?" + "so=" + base64Encode(search_object) + "&rt=" + dojo.byId("acq-unified-result-type").getValue() + "&c=" + dojo.byId("acq-unified-conjunction").getValue(); + } }; this.search = function(uriManager, termManager) { @@ -928,7 +949,12 @@ function URIManager() { /* onload */ openils.Util.addOnLoad( function() { + + // onload handled by external user + if (window.unifiedSearchExternalMode) return; + termManager = new TermManager(); + resultManager = new ResultManager( new LiTablePager(null, new AcqLiTable()), dijit.byId("acq-unified-po-grid"), diff --git a/docs/RELEASE_NOTES_NEXT/acq-invoice-li-search.txt b/docs/RELEASE_NOTES_NEXT/acq-invoice-li-search.txt new file mode 100644 index 0000000000..3ef304d075 --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/acq-invoice-li-search.txt @@ -0,0 +1,22 @@ +ACQ Invoice Inline Lineitem Search and Add +------------------------------------------ + +The Invoice UI is how composed of two tabs, the main invoice tab and a new Search tab. The search tab consists of a subset of the Acquisitions unified search interface. The goal is to allow users to search for lineitems to invoice. Search results may be added directly to the growing invoice. A number of small usability features are included. + +Features +~~~~~~~~ + + * Option (default) to limit searches to invoiceable items. + ** These are lineitems that are not cancelled, have at least one invoiceable copy, linked to a PO whose provider matches that of the current invoice, and are not already linked to the current invoice. + * Search defaults to last-run search (on workstation). + * New Lineitem Detail filter options + * Sort searches by lineitem number (default) and title. + * There is a new Expected Cost field which includes both the total invoiced cost plus the anticipated cost of lineitems as they are added. + * New Price per Copy field + * Lineitem count field + * Show / Hide Invoice details button. Details are displayed by default, but hidden when the user enters the search tab. From there it remains hidden until manually shown (or a new invoice is opened). + * A new "Save & Clear" button which saves the current invoice then clears the invoice display to create a new invoice. + * Provider, shipper, and receiver fields are auto-populated from the first-added invoice data (when not already set). + * Totals are now read-only, since they are derived from existing data (and are informational only). + +