1 dojo.require('dojo.date.locale');
2 dojo.require('dojo.date.stamp');
3 dojo.require('dijit.form.CheckBox');
4 dojo.require('dijit.form.CurrencyTextBox');
5 dojo.require('dijit.form.NumberTextBox');
6 dojo.require('openils.User');
7 dojo.require('openils.Util');
8 dojo.require('openils.CGI');
9 dojo.require('openils.PermaCrud');
10 dojo.require('openils.widget.EditPane');
11 dojo.require('openils.widget.AutoFieldWidget');
12 dojo.require('openils.widget.ProgressDialog');
13 dojo.require('openils.acq.Lineitem');
15 dojo.requireLocalization('openils.acq', 'acq');
16 var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
18 var fundLabelFormat = ['${0} (${1})', 'code', 'year'];
19 var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
21 var cgi = new openils.CGI();
22 var pcrud = new openils.PermaCrud();
37 var widgetRegistry = {acqie : {}, acqii : {}};
40 function nodeByName(name, context) {
41 return dojo.query('[name='+name+']', context)[0];
46 attachLi = cgi.param('attach_li') || [];
47 if (!dojo.isArray(attachLi))
48 attachLi = [attachLi];
50 attachPo = cgi.param('attach_po') || [];
51 if (!dojo.isArray(attachPo))
52 attachPo = [attachPo];
54 focusLineitem = new openils.CGI().param('focus_li');
56 itemTypes = pcrud.retrieveAll('aiit');
58 if(cgi.param('create')) {
62 fieldmapper.standardRequest(
63 ['open-ils.acq', 'open-ils.acq.invoice.retrieve.authoritative'],
65 params : [openils.User.authtoken, invoiceId],
66 oncomplete : function(r) {
67 invoice = openils.Util.readResponse(r);
74 extraCopiesFund = new openils.widget.AutoFieldWidget({
77 searchFilter : {active : 't'},
78 labelFormat : fundLabelFormat,
79 searchFormat : fundSearchFormat,
80 dijitArgs : {required : true},
81 parentNode : dojo.byId('acq-invoice-extra-copies-fund')
83 extraCopiesFund.build();
86 function renderInvoice() {
88 // in create mode, let the LI or PO render the invoice with seed data
89 if( !(cgi.param('create') && (attachPo.length || attachLi.length)) ) {
90 invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), invoice);
93 dojo.byId('acq-invoice-new-item').onclick = function() {
94 var item = new fieldmapper.acqii();
102 if(invoice && openils.Util.isTrue(invoice.complete())) {
104 dojo.forEach( // hide widgets that should not be visible for a completed invoice
105 dojo.query('.hide-complete'),
106 function(node) { openils.Util.hide(node); }
109 new openils.User().getPermOrgList(
110 'ACQ_INVOICE_REOPEN',
112 if(orgs.indexOf(invoice.receiver()) >= 0)
113 openils.Util.show('acq-invoice-reopen-button-wrapper', 'inline');
124 addInvoiceItem(item);
131 addInvoiceEntry(entry);
136 if(attachLi.length) doAttachLi();
137 if(attachPo.length) doAttachPo(0);
140 function doAttachLi() {
142 //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()};
143 if(cgi.param('create')) {
145 // use the first LI in the list to determine the default provider
146 fieldmapper.standardRequest(
147 ['open-ils.acq', 'open-ils.acq.lineitem.retrieve.authoritative'],
149 params : [openils.User.authtoken, attachLi[0], {clear_marc:1}],
150 oncomplete : function(r) {
151 var li = openils.Util.readResponse(r);
152 invoicePane = drawInvoicePane(
153 dojo.byId('acq-view-invoice-div'), null,
154 {provider : li.provider(), shipper : li.provider()}
161 dojo.forEach(attachLi,
163 var entry = new fieldmapper.acqie();
164 entry.id(virtualId--);
167 addInvoiceEntry(entry);
172 function doAttachPo(idx) {
174 if (idx == attachPo.length) return;
175 var poId = attachPo[idx];
177 fieldmapper.standardRequest(
178 ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
181 openils.User.authtoken, poId,
182 {flesh_lineitem_ids : true, flesh_po_items : true}
184 oncomplete: function(r) {
185 var po = openils.Util.readResponse(r);
187 if(cgi.param('create') && idx == 0) {
188 // render the invoice using some seed data from the first PO
189 var invoiceArgs = {provider : po.provider(), shipper : po.provider()};
190 invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
193 dojo.forEach(po.lineitems(),
195 var entry = new fieldmapper.acqie();
196 entry.id(virtualId--);
198 entry.lineitem(lineitem);
199 entry.purchase_order(po);
200 addInvoiceEntry(entry);
204 dojo.forEach(po.po_items(),
206 var item = new fieldmapper.acqii();
207 item.id(virtualId--);
209 item.fund(poItem.fund());
210 item.title(poItem.title());
211 item.author(poItem.author());
212 item.note(poItem.note());
213 item.inv_item_type(poItem.inv_item_type());
214 item.purchase_order(po);
215 item.po_item(poItem);
216 addInvoiceItem(item);
226 function updateTotalCost() {
229 for(var id in widgetRegistry.acqii)
230 if(!widgetRegistry.acqii[id]._object.isdeleted())
231 totalCost += Number(widgetRegistry.acqii[id].cost_billed.getFormattedValue());
232 for(var id in widgetRegistry.acqie)
233 if(!widgetRegistry.acqie[id]._object.isdeleted())
234 totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
235 totalInvoicedBox.attr('value', totalCost);
238 for(var id in widgetRegistry.acqii)
239 if(!widgetRegistry.acqii[id]._object.isdeleted())
240 totalPaid += Number(widgetRegistry.acqii[id].amount_paid.getFormattedValue());
241 for(var id in widgetRegistry.acqie)
242 if(!widgetRegistry.acqie[id]._object.isdeleted())
243 totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
244 totalPaidBox.attr('value', totalPaid);
246 var buttonsDisabled = false;
248 if(totalPaid > totalCost || totalPaid < 0) {
249 openils.Util.addCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
250 invoiceSaveButton.attr('disabled', true);
251 invoiceProrateButton.attr('disabled', true);
252 buttonsDisabled = true;
254 openils.Util.removeCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
255 invoiceSaveButton.attr('disabled', false);
256 invoiceProrateButton.attr('disabled', false);
260 openils.Util.addCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
261 invoiceSaveButton.attr('disabled', true);
262 invoiceProrateButton.attr('disabled', true);
264 openils.Util.removeCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
265 if(!buttonsDisabled) {
266 invoiceSaveButton.attr('disabled', false);
267 invoiceProrateButton.attr('disabled', false);
271 if(totalPaid == totalCost) { // XXX: too rigid?
272 invoiceCloseButton.attr('disabled', false);
274 invoiceCloseButton.attr('disabled', true);
277 balanceOwedBox.attr('value', (totalCost - totalPaid));
281 function registerWidget(obj, field, widget, callback) {
282 var blob = widgetRegistry[obj.classname];
284 blob[obj.id()] = {_object : obj};
285 blob[obj.id()][field] = widget;
288 dojo.connect(w, 'onChange',
294 if(callback) callback(w, ww);
300 function addInvoiceItem(item) {
301 itemTbody = dojo.byId('acq-invoice-item-tbody');
302 if(itemTemplate == null) {
303 itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
306 var row = itemTemplate.cloneNode(true);
307 var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
310 ['title', 'author', 'cost_billed', 'amount_paid'],
314 if(field == 'title' || field == 'author') {
315 //args = {style : 'width:10em'};
316 } else if(field == 'cost_billed' || field == 'amount_paid') {
317 args = {required : true, style : 'width: 8em'};
322 new openils.widget.AutoFieldWidget({
326 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
328 parentNode : nodeByName(field, row)
335 /* ----------- fund -------------- */
340 labelFormat : fundLabelFormat,
341 searchFormat : fundSearchFormat,
342 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
343 dijitArgs : {required : true},
344 parentNode : nodeByName('fund', row)
347 if(item.fund_debit()) {
348 fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
350 fundArgs.searchFilter = {active : 't'}
351 if(itemType && openils.Util.isTrue(itemType.prorate()))
352 fundArgs.dijitArgs = {disabled : true};
355 var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
356 registerWidget(item, 'fund', fundWidget);
358 /* ---------- inv_item_type ------------- */
362 // read-only item view for items that were the result of a po-item
363 var po = item.purchase_order();
364 var po_item = item.po_item();
365 var node = nodeByName('inv_item_type', row);
366 var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
367 orderDate = (!po.order_date()) ? '' :
368 dojo.date.locale.format(dojo.date.stamp.fromISOString(po.order_date()), {selector:'date'});
370 node.innerHTML = dojo.string.substitute(
371 localeStrings.INVOICE_ITEM_PO_DETAILS,
378 po_item.estimated_cost()
387 new openils.widget.AutoFieldWidget({
389 fmField : 'inv_item_type',
390 parentNode : nodeByName('inv_item_type', row),
391 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
392 dijitArgs : {required : true}
395 // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
396 // since this charge will be prorated against (potentially) multiple funds
397 dojo.connect(w, 'onChange',
399 if(!item.fund_debit()) {
400 var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
401 if(!itemType) return;
402 if(openils.Util.isTrue(itemType.prorate())) {
403 fundWidget.widget.attr('disabled', true);
404 fundWidget.widget.attr('value', '');
406 fundWidget.widget.attr('disabled', false);
415 nodeByName('delete', row).onclick = function() {
416 var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
417 var msg = dojo.string.substitute(
418 localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
420 widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue() || ''
423 if(!confirm(msg)) return;
424 itemTbody.removeChild(row);
425 item.isdeleted(true);
427 delete widgetRegistry.acqii[item.id()];
431 itemTbody.appendChild(row);
435 function updateReceiveLink(li) {
437 return; /* can't do this with unsaved invoices */
439 var link = dojo.byId("acq-view-invoice-receive-link");
440 if (link.onclick) return; /* only need to do this once */
442 /* don't do this if there's nothing receivable on the lineitem */
443 if (li.order_summary().recv_count() + li.order_summary().cancel_count() >=
444 li.order_summary().item_count())
447 openils.Util.show("acq-view-invoice-receive");
448 link.onclick = function() { location.href = oilsBasePath + '/acq/invoice/receive/' + invoiceId; };
452 * Ensures focusLineitem is in view and causes a brief
453 * border around the lineitem to come to life then fade.
456 if (!focusLineitem) return;
458 // set during addLineitem()
459 var node = dojo.byId('li-title-ref-' + focusLineitem);
461 console.log('focus: li-title-ref-' + focusLineitem + ' : ' + node);
463 // LI may not yet be rendered
466 console.log('focusing ' + focusLineitem);
468 // prevent numerous re-focuses
469 focusLineitem = null;
471 // causes the full row to be visible
472 dijit.scrollIntoView(node);
474 dojo.require('dojox.fx');
478 dojox.fx.highlight({color : '#BB4433', node : node, duration : 2000}).play();
484 function addInvoiceEntry(entry) {
486 openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
487 openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
488 openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
490 entryTbody = dojo.byId('acq-invoice-entry-tbody');
491 if(entryTemplate == null) {
492 entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
495 if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
496 // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
499 var row = entryTemplate.cloneNode(true);
500 row.setAttribute('lineitem', entry.lineitem());
502 openils.acq.Lineitem.fetchAndRender(
503 entry.lineitem(), {},
506 entry.purchase_order(li.purchase_order());
507 nodeByName('title_details', row).innerHTML = html;
509 nodeByName('title_details', row).parentNode.id = 'li-title-ref-' + li.id();
510 console.log(dojo.byId('li-title-ref-' + li.id()));
512 updateReceiveLink(li);
515 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
517 var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
518 if(!field.match(/count/)) dijitArgs.style = 'width:9em';
519 if(entry.isnew() && field == 'phys_item_count') {
520 // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
521 var count = Number(li.order_summary().item_count() || 0) -
522 Number(li.order_summary().cancel_count() || 0) -
523 Number(li.order_summary().invoice_count() || 0);
524 if(count < 0) count = 0;
525 dijitArgs.value = count;
530 new openils.widget.AutoFieldWidget({
534 dijitArgs : dijitArgs,
535 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
536 parentNode : nodeByName(field, row)
539 if(field == 'phys_item_count') {
540 dojo.connect(w, 'onChange',
542 // staff entered a higher number in the receive field than was originally ordered
543 // taking into account already invoiced items
544 var extra = Number(this.attr('value')) -
545 (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
547 storeExtraCopies(entry, extra);
557 if (focusLineitem == li.id())
562 nodeByName('detach', row).onclick = function() {
563 var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
565 dojo.forEach(['isbn', 'upc', 'issn'],
567 var val = liMarcAttr(entry.lineitem(), ident);
568 if(val) idents.push(val);
572 var msg = dojo.string.substitute(
573 localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
575 liMarcAttr(entry.lineitem(), 'title'),
576 liMarcAttr(entry.lineitem(), 'author'),
580 if(!confirm(msg)) return;
581 entryTbody.removeChild(row);
582 entry.isdeleted(true);
584 delete widgetRegistry.acqie[entry.id()];
588 entryTbody.appendChild(row);
592 function liMarcAttr(lineitem, name) {
593 var attr = lineitem.attributes().filter(
596 attr.attr_type() == 'lineitem_marc_attr_definition' &&
597 attr.attr_name() == name)
601 return (attr) ? attr.attr_value() : '';
604 function saveChanges(doProrate, doClose, doReopen) {
607 saveChangesPartTwo(doProrate, doClose, doReopen);
612 function saveChangesPartTwo(doProrate, doClose, doReopen) {
614 progressDialog.show(true);
617 invoice.complete('f');
622 var updateItems = [];
623 for(var id in widgetRegistry.acqii) {
624 var reg = widgetRegistry.acqii[id];
625 var item = reg._object;
626 if(item.ischanged() || item.isnew() || item.isdeleted()) {
627 updateItems.push(item);
628 if(item.isnew()) item.id(null);
629 for(var field in reg) {
630 if(field != '_object')
631 item[field]( reg[field].getFormattedValue() );
635 if(item.purchase_order() != null && typeof item.purchase_order() == 'object')
636 item.purchase_order( item.purchase_order().id() );
640 var updateEntries = [];
641 for(var id in widgetRegistry.acqie) {
642 var reg = widgetRegistry.acqie[id];
643 var entry = reg._object;
644 if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
645 entry.lineitem(entry.lineitem().id());
646 entry.purchase_order(entry.purchase_order().id());
647 updateEntries.push(entry);
648 if(entry.isnew()) entry.id(null);
650 for(var field in reg) {
651 if(field != '_object')
652 entry[field]( reg[field].getFormattedValue() );
656 dojo.forEach(['purchase_order', 'lineitem'],
658 if(entry[field]() != null && typeof entry[field]() == 'object')
659 entry[field]( entry[field]().id() );
666 invoice = new fieldmapper.acqinv();
669 invoice.ischanged(true); // for now, just always update
672 dojo.forEach(invoicePane.fieldList,
674 invoice[field.name]( field.widget.getFormattedValue() );
679 invoice.complete('t');
682 fieldmapper.standardRequest(
683 ['open-ils.acq', 'open-ils.acq.invoice.update'],
685 params : [openils.User.authtoken, invoice, updateEntries, updateItems],
686 oncomplete : function(r) {
687 progressDialog.hide();
688 var invoice = openils.Util.readResponse(r);
691 return prorateInvoice(invoice);
692 location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
699 function prorateInvoice(invoice) {
700 if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
701 progressDialog.show(true);
703 fieldmapper.standardRequest(
704 ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
706 params : [openils.User.authtoken, invoice.id()],
707 oncomplete : function(r) {
708 progressDialog.hide();
709 var invoice = openils.Util.readResponse(r);
711 location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
718 function storeExtraCopies(entry, numExtra) {
720 dojo.byId('acq-invoice-extra-copies-message').innerHTML =
721 dojo.string.substitute(
722 localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
725 addCopyHandler = dojo.connect(
729 extraCopies[entry.lineitem().id()] = {
731 fund : extraCopiesFund.widget.attr('value')
733 extraItemsDialog.hide();
734 dojo.disconnect(addCopyHandler);
742 widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
743 extraItemsDialog.hide()
747 extraItemsDialog.show();
750 function createExtraCopies(oncomplete) {
753 for(var liId in extraCopies) {
754 var data = extraCopies[liId];
755 for(var i = 0; i < data.numExtra; i++) {
756 var lid = new fieldmapper.acqlid();
760 lid.recv_time('now');
768 fieldmapper.standardRequest(
769 ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
771 params : [openils.User.authtoken, lids, true],
772 oncomplete : function(r) {
773 if(openils.Util.readResponse(r))
782 openils.Util.addOnLoad(init);