]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
4fbc16a84670d2b4526933d5b519afae975b6296
[Evergreen.git] / Open-ILS / web / js / ui / default / acq / invoice / view.js
1 dojo.require('dojo.date.locale');
2 dojo.require('dojo.date.stamp');
3 dojo.require('dojo.cookie');
4 dojo.require('dijit.form.CheckBox');
5 dojo.require('dijit.form.Button');
6 dojo.require('dijit.form.CurrencyTextBox');
7 dojo.require('dijit.form.NumberTextBox');
8 dojo.require('openils.User');
9 dojo.require('openils.Util');
10 dojo.require('openils.CGI');
11 dojo.require('openils.PermaCrud');
12 dojo.require('openils.widget.EditPane');
13 dojo.require('openils.widget.AutoFieldWidget');
14 dojo.require('openils.widget.ProgressDialog');
15 dojo.require('openils.acq.Lineitem');
16 dojo.require('openils.XUL');
17
18 dojo.requireLocalization('openils.acq', 'acq');
19 var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
20
21 var fundLabelFormat = ['${0} (${1})', 'code', 'year'];
22 var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
23 var fundSearchFilter = {active : 't'};
24
25 var cgi = new openils.CGI();
26 var pcrud = new openils.PermaCrud();
27 var attachLi;
28 var attachPo;
29 var invoice;
30 var itemTbody;
31 var itemTemplate;
32 var entryTemplate;
33 var totalInvoicedBox;
34 var totalPaidBox;
35 var balanceOwedBox;
36 var invoicePane;
37 var itemTypes;
38 var virtualId = -1;
39 var extraCopies = {};
40 var extraCopiesFund;
41 var widgetRegistry = {acqie : {}, acqii : {}};
42 var focusLineitem;
43 var searchInitDone = false;
44 var termManager;
45 var resultManager;
46 var finalizePos = [];
47
48 function nodeByName(name, context) {
49     return dojo.query('[name='+name+']', context)[0];
50 }
51
52 function init() {
53     // before rendering any fund selectors, limit the funds to 
54     // attempt to retrieve to those the user can actually use.
55     new openils.User().getPermOrgList(
56         ['ADMIN_INVOICE','CREATE_INVOICE','MANAGE_FUND'],
57         function(orgs) { 
58             fundSearchFilter.org = orgs;
59             init2();
60         },
61         true, true // descendants, id_list
62     );
63 }
64
65 function init2() {
66
67     attachLi = cgi.param('attach_li') || [];
68     if (!dojo.isArray(attachLi)) 
69         attachLi = [attachLi];
70
71     attachPo = cgi.param('attach_po') || [];
72     if (!dojo.isArray(attachPo)) 
73         attachPo = [attachPo];
74
75     focusLineitem = new openils.CGI().param('focus_li');
76
77     totalInvoicedBox = dojo.byId('acq-total-invoiced-box');
78     totalPaidBox = dojo.byId('acq-total-paid-box');
79     balanceOwedBox = dojo.byId('acq-total-balance-box');
80
81     itemTypes = pcrud.retrieveAll('aiit');
82
83     dojo.byId('acq-invoice-summary-toggle-off').onclick = function() {
84         openils.Util.hide(dojo.byId('acq-invoice-summary'));
85         openils.Util.show(dojo.byId('acq-invoice-summary-small'));
86     };
87
88     dojo.byId('acq-invoice-summary-toggle-on').onclick = function() {
89         openils.Util.show(dojo.byId('acq-invoice-summary'));
90         openils.Util.hide(dojo.byId('acq-invoice-summary-small'));
91     }
92
93     if(cgi.param('create')) {
94         renderInvoice();
95
96         // show summary info by default for new invoices
97         dojo.byId('acq-invoice-summary-toggle-on').onclick();
98
99     } else {
100         dojo.byId('acq-invoice-summary-toggle-off').onclick();
101         fieldmapper.standardRequest(
102             ['open-ils.acq', 'open-ils.acq.invoice.retrieve.authoritative'],
103             {
104                 params : [openils.User.authtoken, invoiceId],
105                 oncomplete : function(r) {
106                     invoice = openils.Util.readResponse(r);     
107                     renderInvoice();
108                 }
109             }
110         );
111     }
112
113     extraCopiesFund = new openils.widget.AutoFieldWidget({
114         fmField : 'fund',
115         fmClass : 'acqlid',
116         labelFormat : fundLabelFormat,
117         searchFormat : fundSearchFormat,
118         searchFilter : fundSearchFilter,
119         dijitArgs : {required : true},
120         parentNode : dojo.byId('acq-invoice-extra-copies-fund')
121     });
122     extraCopiesFund.build();
123 }
124
125 function renderInvoice() {
126
127     // in create mode, let the LI or PO render the invoice with seed data
128     if( !(cgi.param('create') && (attachPo.length || attachLi.length)) ) {
129         invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), invoice);
130     }
131
132     dojo.byId('acq-invoice-new-item').onclick = function() {
133         var item = new fieldmapper.acqii();
134         item.id(virtualId--);
135         item.isnew(true);
136         addInvoiceItem(item);
137     }
138
139     updateTotalCost();
140
141     if(invoice && openils.Util.isTrue(invoice.complete())) {
142
143         dojo.forEach( // hide widgets that should not be visible for a completed invoice
144             dojo.query('.hide-complete'), 
145             function(node) { openils.Util.hide(node); }
146         );
147
148         new openils.User().getPermOrgList(
149             'ACQ_INVOICE_REOPEN', 
150             function (orgs) {
151                 if(orgs.indexOf(invoice.receiver()) >= 0)
152                     openils.Util.show('acq-invoice-reopen-button-wrapper', 'inline');
153             }, 
154             true, 
155             true
156         );
157     }
158
159     // display items and entries in ID order 
160     // which effectively equates to add order.
161     function idsort(a, b) { return a.id() < b.id() ? -1 : 1 }
162
163     if(invoice) {
164         dojo.forEach(
165             invoice.items().sort(idsort),
166             function(item) {
167                 addInvoiceItem(item);
168             }
169         );
170
171         dojo.forEach(
172             invoice.entries().sort(idsort),
173             function(entry) {
174                 addInvoiceEntry(entry);
175             }
176         );
177     }
178
179     if(attachLi.length) doAttachLi();
180     if(attachPo.length) doAttachPo(0);
181 }
182
183 function doAttachLi(skipInit) {
184
185     //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; 
186     if(cgi.param('create') && !skipInit) {
187
188         // use the first LI in the list to determine the default provider
189         fieldmapper.standardRequest(
190             ['open-ils.acq', 'open-ils.acq.lineitem.retrieve.authoritative'],
191             {
192                 params : [openils.User.authtoken, attachLi[0], {clear_marc:1}],
193                 oncomplete : function(r) {
194                     var li = openils.Util.readResponse(r);
195                     invoicePane = drawInvoicePane(
196                         dojo.byId('acq-view-invoice-div'), null, 
197                         {provider : li.provider(), shipper : li.provider()}
198                     );
199                 }
200             }
201         );
202     }
203
204     dojo.forEach(attachLi,
205         function(li) {
206             var entry = new fieldmapper.acqie();
207             entry.id(virtualId--);
208             entry.isnew(true);
209             entry.lineitem(li);
210             addInvoiceEntry(entry);
211         }
212     );
213 }
214
215 function doAttachPo(idx) {
216
217     if (idx == attachPo.length) return;
218     var poId = attachPo[idx];
219
220     fieldmapper.standardRequest(
221         ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
222         {   async: true,
223             params: [
224                 openils.User.authtoken, poId,
225                 {flesh_lineitem_ids : true, flesh_po_items : true}
226             ],
227             oncomplete: function(r) {
228                 var po = openils.Util.readResponse(r);
229
230                 if(cgi.param('create') && idx == 0) {
231                     // render the invoice using some seed data from the first PO
232                     var invoiceArgs = {provider : po.provider(), shipper : po.provider()}; 
233                     invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
234                 }
235
236                 dojo.forEach(po.lineitems(), 
237                     function(lineitem) {
238                         var entry = new fieldmapper.acqie();
239                         entry.id(virtualId--);
240                         entry.isnew(true);
241                         entry.lineitem(lineitem);
242                         entry.purchase_order(po);
243                         addInvoiceEntry(entry);
244                     }
245                 );
246
247                 dojo.forEach(po.po_items(),
248                     function(poItem) {
249                         var item = new fieldmapper.acqii();
250                         item.id(virtualId--);
251                         item.isnew(true);
252                         item.fund(poItem.fund());
253                         item.title(poItem.title());
254                         item.author(poItem.author());
255                         item.note(poItem.note());
256                         item.inv_item_type(poItem.inv_item_type());
257                         item.purchase_order(po);
258                         item.po_item(poItem);
259                         addInvoiceItem(item);
260                     }
261                 );
262
263                 doAttachPo(++idx);
264             }
265         }
266     );
267 }
268
269 // XUL cookie bits
270 var cookieUriSSL, cookieSvc, cookieMgr;
271
272 function performSearch(pageDir, clearFirst) {
273     if (clearFirst)
274         clearSearchResTable(); 
275
276     var searchObject = termManager.buildSearchObject();
277
278     if (openils.XUL.isXUL()) {
279
280         cookieSvc.setCookieString(cookieUriSSL, null, 
281             "invs=" + base64Encode(searchObject) + ';max-age=2592000', null);
282
283         cookieSvc.setCookieString(cookieUriSSL, null, 
284             "invc=" + dojo.byId("acq-unified-conjunction").getValue() + 
285                 ';max-age=2592000', null);
286
287     } else {
288
289         dojo.cookie('invs', base64Encode(searchObject));
290         dojo.cookie('invc', dojo.byId("acq-unified-conjunction").getValue());
291     }
292
293     if (pageDir == 0) { // new search
294         resultsLoader.displayOffset = 0;
295     } else {
296         resultsLoader.displayOffset += pageDir * resultsLoader.displayLimit;
297     }
298
299     if (resultsLoader.displayOffset == 0) {
300         openils.Util.hide('acq-inv-search-prev');
301     } else {
302         openils.Util.show('acq-inv-search-prev', 'inline');
303     }
304
305     if (dojo.byId('acq-invoice-search-limit-invoiceable').checked) {
306         if (!searchObject.jub) 
307             searchObject.jub = [];
308
309         // exclude lineitems that are "cancelled" (sidebar: 'Mericans spell it 'canceled')
310         searchObject.jub.push({state : 'cancelled', '__not' : true});
311
312         // exclude lineitems already linked to this invoice
313         if (invoice && invoice.id() > 0) { 
314             if (!searchObject.acqinv)
315                 searchObject.acqinv = [];
316             searchObject.acqinv.push({id : invoice.id(), '__not' : true});
317         }
318
319         // limit to lineitems that have invoiceable copies
320         searchObject.acqlisumi = [{item_count : 1, '_gte' : true}];
321
322         // limit to provider if a provider is selected
323         var provider = invoicePane.getFieldValue('provider');
324         if (provider) {
325             if (!searchObject.jub.filter(function(i) { return i.provider != null }).length)
326                 searchObject.jub.push({provider : provider});
327         }
328     }
329
330     if (dojo.byId('acq-invoice-search-sort-title').checked) {
331         uriManager.order_by = 
332             [ {"class": "acqlia", "field":"attr_value", "transform":"first"} ];
333     }
334
335     resultsLoader.lastSearch = searchObject;
336     resultManager.go(searchObject)
337     console.log('Lineitem Search: ' + js2JSON(searchObject));
338     focusLastSearchInput();
339 }
340
341
342 function renderUnifiedSearch() {
343
344     if (!searchInitDone) {
345
346         searchInitDone = true;
347         termManager = new TermManager();
348         resultManager = new ResultManager();
349         resultsLoader = new searchResultsLoader();
350         uriManager = new URIManager();
351
352         // define custom lineitem result handler
353         resultManager.result_types = {
354             "lineitem": {
355                 "search_options": { "id_list": true },
356                 "revealer": function() { },
357                 "finisher": function() {
358                     resultsLoader.batch_length = resultManager.count_results;
359                 },
360                 "adder": function(li) {
361                     resultsLoader.addLineitem(li);
362                 },
363                 "interface": resultsLoader
364             },
365             "no_results": {
366                 "revealer": function() { }
367             }
368
369         };
370
371         resultManager.no_results_popup = true;
372         resultManager.submitter = smartSearchSubmitter;
373
374         var searchObject, searchConjunction;
375
376         if (openils.XUL.isXUL()) {
377     
378             if (!cookieSvc) {
379
380                 var ios = Components.classes["@mozilla.org/network/io-service;1"]
381                     .getService(Components.interfaces.nsIIOService);
382
383                 cookieUriSSL = ios.newURI("https://" + location.hostname, null, null);
384     
385                 cookieSvc = Components.classes["@mozilla.org/cookieService;1"]
386                     .getService(Components.interfaces.nsICookieService);
387
388
389                 cookieManager = Components.classes["@mozilla.org/cookiemanager;1"]
390                     .getService(Components.interfaces.nsICookieManager);
391             }
392
393             var iter = cookieManager.enumerator;
394             while (iter.hasMoreElements()) {
395                 var cookie = iter.getNext();
396                 if (cookie instanceof Components.interfaces.nsICookie) {
397                     if (cookie.name == 'invs')
398                         searchObject = cookie.value;
399                     if (cookie.name == 'invc')
400                         searchConjunction = cookie.value;
401                 }
402             }
403
404         } else {
405             // useful for web-based testing
406             searchObject = dojo.cookie('invs');
407             searchConjunction = dojo.cookie('invc');
408         }
409
410         if (searchObject) {
411
412             // if there is a search object cookie, populate the search form
413             termManager.reflect(base64Decode(searchObject));
414             dojo.byId("acq-unified-conjunction").setValue(searchConjunction);
415
416         } else {
417             console.log('adding row');
418             termManager.addRow();
419         }
420     }
421
422     dojo.addClass(dojo.byId('oils-acq-invoice-table'), 'hidden');
423     dojo.removeClass(dojo.byId('oils-acq-invoice-search'), 'hidden');
424     focusLastSearchInput();
425 }
426
427 function focusLastSearchInput() {
428     // TODO: see about making this better and moving it into search/unified.js
429     var wnodes = dojo.query('[name=widget]');
430     var inputNode = wnodes.item(wnodes.length - 1).firstChild;
431     if (inputNode) {
432         try {
433             inputNode.select();
434         } catch(E) {
435             inputNode.focus();
436         }
437     }
438 }
439
440 var resultsTbody, resultsRow;
441 function searchResultsLoader() {
442     this.displayOffset = 0;
443     this.displayLimit = 10;
444
445     if (!resultsTbody) {
446         resultsTbody = dojo.byId('acq-invoice-search-results-tbody');
447         resultsRow = resultsTbody.removeChild(dojo.byId('acq-invoice-search-results-tr'));
448     }
449
450     this.addLineitem = function(li_id) {
451         console.log('Adding search result lineitem ' + li_id);
452         var row = resultsRow.cloneNode(true);
453         resultsTbody.appendChild(row);
454         var checkbox = dojo.query('[name=search-results-checkbox]', row)[0];
455         checkbox.setAttribute('lineitem', li_id);
456
457         // this lineitem is already part of the invoice
458         if (dojo.query('[entry_lineitem_row=' + li_id + ']')[0]) {
459             checkbox.disabled = true;
460             dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced');
461         }
462
463         openils.acq.Lineitem.fetchAndRender(
464             li_id, {}, 
465             function(li, html) { 
466                 dojo.query('[name=search-results-content-div]', row)[0].innerHTML = html;
467             }
468         );
469     }
470 }
471
472 function addSelectedToInvoice() {
473     var inputs = dojo.query('[name=search-results-checkbox]');
474     attachLi = [];
475     dojo.forEach(inputs,
476         function(checkbox) {
477             if (checkbox.checked) {
478                 attachLi.push(checkbox.getAttribute('lineitem'));
479                 checkbox.disabled = true;
480                 checkbox.checked = false;
481                 dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced');
482             }
483         }
484     );
485     doAttachLi(true);
486 }
487
488 allResSelected = false;
489 function clearSearchResTable() {
490     allResSelected = false;
491     while (resultsTbody.childNodes[0])
492         resultsTbody.removeChild(resultsTbody.childNodes[0]);
493 }
494
495 function selectSearchResults() {
496     allResSelected = !allResSelected;
497     dojo.query('[name=search-results-checkbox]').forEach(
498         function(input) { input.checked = allResSelected });
499 }
500
501 function updateTotalCost() {
502
503     var totalCost = 0;    
504     for(var id in widgetRegistry.acqii) 
505         if(!widgetRegistry.acqii[id]._object.isdeleted())
506             totalCost += Number(widgetRegistry.acqii[id].cost_billed.getFormattedValue());
507     for(var id in widgetRegistry.acqie) 
508         if(!widgetRegistry.acqie[id]._object.isdeleted())
509             totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
510     totalInvoicedBox.innerHTML = totalCost.toFixed(2);
511
512     totalPaid = 0;    
513     for(var id in widgetRegistry.acqii) 
514         if(!widgetRegistry.acqii[id]._object.isdeleted())
515             totalPaid += Number(widgetRegistry.acqii[id].amount_paid.getFormattedValue());
516     for(var id in widgetRegistry.acqie) 
517         if(!widgetRegistry.acqie[id]._object.isdeleted())
518             totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
519     totalPaidBox.innerHTML = totalPaid.toFixed(2);
520
521     var buttonsDisabled = false;
522
523     if(totalPaid > totalCost || totalPaid < 0) {
524         openils.Util.addCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
525         invoiceSaveButton.attr('disabled', true);
526         invoiceProrateButton.attr('disabled', true);
527         buttonsDisabled = true;
528     } else {
529         openils.Util.removeCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
530         invoiceSaveButton.attr('disabled', false);
531         invoiceProrateButton.attr('disabled', false);
532     }
533
534     if(totalCost < 0) {
535         openils.Util.addCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
536         invoiceSaveButton.attr('disabled', true);
537         invoiceProrateButton.attr('disabled', true);
538     } else {
539         openils.Util.removeCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
540         if(!buttonsDisabled) {
541             invoiceSaveButton.attr('disabled', false);
542             invoiceProrateButton.attr('disabled', false);
543         }
544     }
545
546     if(totalPaid == totalCost) { // XXX: too rigid?
547         invoiceCloseButton.attr('disabled', false);
548     } else {
549         invoiceCloseButton.attr('disabled', true);
550     }
551
552     balanceOwedBox.innerHTML = (totalCost - totalPaid).toFixed(2);
553
554     updateExpectedCost();
555 }
556
557
558 function registerWidget(obj, field, widget, callback) {
559     var blob = widgetRegistry[obj.classname];
560     if(!blob[obj.id()]) 
561         blob[obj.id()] = {_object : obj};
562     blob[obj.id()][field] = widget;
563     widget.build(
564         function(w, ww) {
565             dojo.connect(w, 'onChange', 
566                 function(newVal) { 
567                     obj.ischanged(true); 
568                     updateTotalCost();
569                 }
570             );
571             if(callback) callback(w, ww);
572         }
573     );
574     return widget;
575 }
576
577 var finalInvTbody, finalInvRow;
578 var finalInvPoSeen = {};
579 function addMarkFinalPO(item, po_item, po_label) {
580
581     if (finalInvPoSeen[po_item.purchase_order()]) return;
582     finalInvPoSeen[po_item.purchase_order()] = true;
583
584     openils.Util.show(dojo.byId('oils-acq-final-invoice-pane'));
585
586     if (!finalInvTbody) {
587         finalInvTbody = dojo.byId('acq-final-invoice-tbody');
588         finalInvRow = finalInvTbody.removeChild(
589             dojo.byId('acq-final-invoice-row'));
590     }
591
592     var row = finalInvRow.cloneNode(true);
593     nodeByName('po-label', row).innerHTML = po_label;
594     var cbox = new dijit.form.CheckBox({}, nodeByName('checkbox', row));
595
596     dojo.connect(cbox, 'onChange', function(set) {
597         if (set) { // add to finalize list
598             finalizePos.push(Number(po_item.purchase_order()));
599         } else { // remove from finalize list
600             finalizePos = finalizePos.filter(
601                 function(id) {return id != po_item.purchase_order()});
602         }
603     });
604
605     finalInvTbody.appendChild(row);
606 }
607
608 function addInvoiceItem(item) {
609     itemTbody = dojo.byId('acq-invoice-item-tbody');
610     if(itemTemplate == null) {
611         itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
612     }
613
614     var row = itemTemplate.cloneNode(true);
615     var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
616
617     dojo.forEach(
618         ['title', 'author', 'cost_billed', 'amount_paid'], 
619         function(field) {
620             
621             var args;
622             if(field == 'title' || field == 'author') {
623                 //args = {style : 'width:10em'};
624             } else if(field == 'cost_billed' || field == 'amount_paid') {
625                 args = {required : true, style : 'width: 8em'};
626             }
627
628             registerWidget(
629                 item,
630                 field,
631                 new openils.widget.AutoFieldWidget({
632                     fmClass : 'acqii',
633                     fmObject : item,
634                     fmField : field,
635                     readOnly : invoice && openils.Util.isTrue(invoice.complete()),
636                     dijitArgs : args,
637                     parentNode : nodeByName(field, row)
638                 }),
639                 function(w) {
640                     if (field == "cost_billed") {
641                         dojo.connect(
642                             w, "onChange", function(value) {
643                                 var paid = widgetRegistry.acqii[item.id()].amount_paid.widget;
644                                 if (value && isNaN(paid.attr("value")))
645                                     paid.attr("value", value);
646                             }
647                         );
648                     }
649                 }
650             );
651         }
652     );
653
654
655     /* ----------- fund -------------- */
656     var fundArgs = {
657         fmClass : 'acqii',
658         fmObject : item,
659         fmField : 'fund',
660         labelFormat : fundLabelFormat,
661         searchFormat : fundSearchFormat,
662         searchFilter : fundSearchFilter,
663         readOnly : invoice && openils.Util.isTrue(invoice.complete()),
664         dijitArgs : {required : true},
665         parentNode : nodeByName('fund', row)
666     }
667
668     if(item.fund_debit()) {
669         fundArgs.searchFilter = {'-or' : [{ "-and": fundSearchFilter }, {id : item.fund()}]};
670     } else {
671         if(itemType && openils.Util.isTrue(itemType.prorate()))
672             fundArgs.dijitArgs = {disabled : true};
673     }
674
675     var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
676     registerWidget(item, 'fund', fundWidget);
677
678     /* ---------- inv_item_type ------------- */
679
680     if(item.po_item()) {
681
682         // read-only item view for items that were the result of a po-item
683         var po = item.purchase_order();
684         var po_item = item.po_item();
685         var node = nodeByName('inv_item_type', row);
686         var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
687         orderDate = (!po.order_date()) ? '' : 
688                 dojo.date.locale.format(dojo.date.stamp.fromISOString(po.order_date()), {selector:'date'});
689
690         node.innerHTML = dojo.string.substitute(
691             localeStrings.INVOICE_ITEM_PO_DETAILS, 
692             [ 
693                 itemType.name(),
694                 oilsBasePath, 
695                 po.id(), 
696                 po.name(), 
697                 orderDate,
698                 po_item.estimated_cost() 
699             ]
700         );
701
702         if (openils.Util.isTrue(itemType.blanket()) 
703                 && po.state() != 'received') {
704
705             fieldmapper.standardRequest(
706                 ['open-ils.acq', 
707                     'open-ils.acq.purchase_order.retrieve.authoritative'],
708                 {   async: true,
709                     params: [openils.User.authtoken, po.id(), {
710                         "flesh_price_summary": true
711                     }],
712                     oncomplete: function(r) {
713                         // update the global PO instead of replacing it, since other 
714                         // code outside our control may be referencing it.
715                         var po2 = openils.Util.readResponse(r);
716
717                         var po_label = dojo.string.substitute(
718                             localeStrings.INVOICE_ITEM_PO_LABEL,
719                             [ oilsBasePath, po2.id(), po2.name(), 
720                               orderDate, po2.amount_estimated().toFixed(2)
721                             ]
722                         );
723
724                         addMarkFinalPO(item, po_item, po_label);
725                     }
726                 }
727             );
728         }
729
730     } else {
731
732         registerWidget(
733             item,
734             'inv_item_type',
735             new openils.widget.AutoFieldWidget({
736                 fmObject : item,
737                 fmField : 'inv_item_type',
738                 parentNode : nodeByName('inv_item_type', row),
739                 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
740                 dijitArgs : {required : true}
741             }),
742             function(w, ww) {
743                 // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
744                 // since this charge will be prorated against (potentially) multiple funds
745                 dojo.connect(w, 'onChange', 
746                     function() {
747                         if(!item.fund_debit()) {
748                             var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
749                             if(!itemType) return;
750                             if(openils.Util.isTrue(itemType.prorate())) {
751                                 fundWidget.widget.attr('disabled', true);
752                                 fundWidget.widget.attr('value', '');
753                             } else {
754                                 fundWidget.widget.attr('disabled', false);
755                             }
756                         }
757                     }
758                 );
759             }
760         );
761     }
762
763     nodeByName('delete', row).onclick = function() {
764         var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
765
766         var iTypeName = '';
767         if (widgetRegistry.acqii[item.id()].inv_item_type) {
768             iTypeName = widgetRegistry.acqii[item.id()]
769                 .inv_item_type.getFormattedValue()
770         } else {
771             // if the invoice_item came from a po_item, the type is
772             // read-only, hence no widget in the registry.  Look up
773             // the name in the cached types list.
774             var itype = itemTypes.filter(
775                 function(t) { return (t.code() == item.inv_item_type()) })[0];
776             iTypeName = itype.name();
777         }
778
779         var msg = dojo.string.substitute(
780             localeStrings.INVOICE_CONFIRM_ITEM_DELETE, 
781             [cost || 0, iTypeName || '']
782         );
783         if(!confirm(msg)) return;
784         itemTbody.removeChild(row);
785         item.isdeleted(true);
786         if(item.isnew())
787             delete widgetRegistry.acqii[item.id()];
788         updateTotalCost();
789     }
790
791     itemTbody.appendChild(row);
792     updateTotalCost();
793 }
794
795 function updateReceiveLink(li) {
796     if (!invoiceId)
797         return; /* can't do this with unsaved invoices */
798
799     var link = dojo.byId("acq-view-invoice-receive-link");
800     if (link.onclick) return; /* only need to do this once */
801
802     /* don't do this if there's nothing receivable on the lineitem */
803     if (li.order_summary().recv_count() + li.order_summary().cancel_count() >=
804         li.order_summary().item_count())
805         return;
806
807     openils.Util.show("acq-view-invoice-receive");
808     link.onclick = function() { location.href =  oilsBasePath + '/acq/invoice/receive/' + invoiceId; };
809 }
810
811 /*
812  * Ensures focusLineitem is in view and causes a brief 
813  * border around the lineitem to come to life then fade.
814  */
815 function focusLi() {
816     if (!focusLineitem) return;
817
818     // set during addLineitem()
819     var node = dojo.byId('li-title-ref-' + focusLineitem);
820
821     console.log('focus: li-title-ref-' + focusLineitem + ' : ' + node);
822
823     // LI may not yet be rendered
824     if (!node) return; 
825
826     console.log('focusing ' + focusLineitem);
827
828     // prevent numerous re-focuses
829     focusLineitem = null; 
830
831     // causes the full row to be visible
832     dijit.scrollIntoView(node);
833
834     dojo.require('dojox.fx');
835
836     setTimeout(
837         function() {
838             dojox.fx.highlight({color : '#BB4433', node : node, duration : 2000}).play();
839         }, 
840     100);
841 }
842
843
844 // expected cost is totalCostInvoiced + totalCostNotYetInvoiced
845 function updateExpectedCost() {
846
847     var cost = Number(totalInvoicedBox.innerHTML || 0);
848
849     // for any LI's that are not yet billed (i.e. filled in)
850     // use the total expected cost for that lineitem.
851     for(var id in widgetRegistry.acqie) {
852         var entry = widgetRegistry.acqie[id]._object;
853         if(!entry.isdeleted()) {
854             if (Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue()) == 0) {
855                 var li = entry.lineitem();
856                 cost += 
857                     Number(li.order_summary().estimated_amount()) - 
858                     Number(li.order_summary().paid_amount());
859             }
860         }
861     }
862
863     dojo.byId('acq-invoice-summary-cost').innerHTML = cost.toFixed(2);
864 }
865
866 var invoicEntryWidgets = {};
867 function addInvoiceEntry(entry) {
868     console.log('Adding new entry for lineitem ' + entry.lineitem());
869
870     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
871     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
872     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
873
874     dojo.byId('acq-invoice-summary-count').innerHTML = 
875         Number(dojo.byId('acq-invoice-summary-count').innerHTML) + 1;
876
877     entryTbody = dojo.byId('acq-invoice-entry-tbody');
878     if(entryTemplate == null) {
879         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
880     }
881
882     if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
883         // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
884         return;
885
886     var row = entryTemplate.cloneNode(true);
887     row.setAttribute('lineitem', entry.lineitem());
888     row.setAttribute('entry_lineitem_row', entry.lineitem());
889
890     openils.acq.Lineitem.fetchAndRender(
891         entry.lineitem(), {}, 
892         function(li, html) { 
893             entry.lineitem(li);
894             entry.purchase_order(li.purchase_order());
895             nodeByName('title_details', row).innerHTML = html;
896
897             nodeByName('title_details', row).parentNode.id = 'li-title-ref-' + li.id();
898             console.log(dojo.byId('li-title-ref-' + li.id()));
899
900             updateReceiveLink(li);
901
902             // set some default values if otherwise unset
903             if (!invoicePane.getFieldValue('receiver')) {
904                 invoicePane.setFieldValue('receiver', li.purchase_order().ordering_agency());
905             }
906             if (!invoicePane.getFieldValue('provider')) {
907                 invoicePane.setFieldValue('provider', li.purchase_order().provider());
908             }
909
910             dojo.forEach(
911                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
912                 function(field) {
913                     var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
914                     if(field.match(/count/)) {
915                         dijitArgs.style = 'width:4em;';
916                     } else {
917                         dijitArgs.style = 'width:9em;';
918                     }
919                     if (entry.isnew() && (field == 'phys_item_count' || field == 'inv_item_count')) {
920                         // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
921                         var count = Number(li.order_summary().item_count() || 0) - 
922                                     Number(li.order_summary().cancel_count() || 0) -
923                                     Number(li.order_summary().invoice_count() || 0);
924                         if(count < 0) count = 0;
925                         dijitArgs.value = count;
926                     }
927                     registerWidget(
928                         entry, 
929                         field,
930                         new openils.widget.AutoFieldWidget({
931                             fmObject : entry,
932                             fmClass : 'acqie',
933                             fmField : field,
934                             dijitArgs : dijitArgs,
935                             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
936                             parentNode : nodeByName(field, row)
937                         }),
938                         function(w) {    
939
940                             if(field == 'phys_item_count') {
941                                 dojo.connect(w, 'onChange', 
942                                     function() {
943                                         // staff entered a higher number in the receive field than was originally ordered
944                                         // taking into account already invoiced items
945                                         var extra = Number(this.attr('value')) - 
946                                             (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
947                                         if(extra > 0) {
948                                             storeExtraCopies(entry, extra);
949                                         }
950                                     }
951                                 )
952                             } // if
953
954                             if (field == "cost_billed") {
955                                 // hooks applied with dojo.connect to dijit events are additive, so there's no conflict between this and what comes next
956                                 dojo.connect(
957                                     w, "onChange", function(value) {
958                                     var paid = widgetRegistry.acqie[entry.id()].amount_paid.widget;
959                                         if (value && isNaN(paid.attr("value")))
960                                             paid.attr("value", value);
961                                     }
962                                 );
963                             }
964                             if(field == 'inv_item_count' || field == 'cost_billed') {
965                                 setPerCopyPrice(row, entry);
966                                 // update the per-copy count as invoice count and cost billed change 
967                                 dojo.connect(w, 'onChange', function() { setPerCopyPrice(row, entry) } );
968                             } 
969
970                         } // func
971                     );
972                 }
973             );
974
975             updateTotalCost();
976             if (focusLineitem == li.id())
977                 focusLi();
978         }
979     );
980
981     nodeByName('detach', row).onclick = function() {
982         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
983         var idents = [];
984         dojo.forEach(['isbn', 'upc', 'issn'], 
985             function(ident) { 
986                 var val = liMarcAttr(entry.lineitem(), ident);
987                 if(val) idents.push(val); 
988             }
989         );
990
991         var msg = dojo.string.substitute(
992             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
993                 cost || 0,
994                 liMarcAttr(entry.lineitem(), 'title'),
995                 liMarcAttr(entry.lineitem(), 'author'),
996                 idents.join(',')
997             ]
998         );
999         if(!confirm(msg)) return;
1000         entryTbody.removeChild(row);
1001         entry.isdeleted(true);
1002         if(entry.isnew())
1003             delete widgetRegistry.acqie[entry.id()];
1004         updateTotalCost();
1005     }
1006
1007     entryTbody.appendChild(row);
1008 }
1009
1010 function setPerCopyPrice(row, entry) {
1011     var inv_w = widgetRegistry.acqie[entry.id()].inv_item_count;
1012     var bill_w = widgetRegistry.acqie[entry.id()].cost_billed;
1013
1014     if (inv_w && bill_w) {
1015         var invoiced = Number(inv_w.getFormattedValue());
1016         var billed = Number(bill_w.getFormattedValue());
1017         console.log(invoiced + ' : ' + billed);
1018         if (invoiced > 0) {
1019             nodeByName('amount_paid_per_copy', row).innerHTML = (billed / invoiced).toFixed(2);
1020         } else {
1021             nodeByName('amount_paid_per_copy', row).innerHTML = '0.00';
1022         }
1023     }
1024 }
1025
1026 function liMarcAttr(lineitem, name) {
1027     var attr = lineitem.attributes().filter(
1028         function(attr) { 
1029             if(
1030                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
1031                 attr.attr_name() == name) 
1032                     return attr 
1033         } 
1034     )[0];
1035     return (attr) ? attr.attr_value() : '';
1036 }
1037
1038 function saveChanges(args) {
1039     args = args || {};
1040     createExtraCopies(function() { saveChangesPartTwo(args); });
1041 }
1042
1043 // Define a helper function to 'unflesh' sub-objects from an fmclass object.
1044 // 'this' specifies the object; the arguments specify a list of names of
1045 // sub-objects.
1046 function unflesh() {
1047     var _, $ = this;
1048     dojo.forEach(arguments, function (n) {
1049         _ = $[n]();
1050         if (_ !== null && typeof _ === 'object')
1051             $[n]( _.id() );
1052     });
1053 }
1054
1055 function saveChangesPartTwo(args) {
1056     args = args || {};
1057
1058     if(args.reopen) {
1059         invoice.complete('f');
1060
1061     } else {
1062
1063         // Prepare an invoice for submission
1064         if(!invoice) {
1065             invoice = new fieldmapper.acqinv();
1066             invoice.isnew(true);
1067         } else {
1068             invoice.ischanged(true); // for now, just always update
1069         }
1070
1071         var e = invoicePane.mapValues(function (n, v) { invoice[n](v); });
1072         if (e instanceof Error) {
1073             alert(e.message);
1074             return;
1075         }
1076
1077         if(args.close)
1078             invoice.complete('t');
1079
1080
1081         // Prepare any charge items
1082         var updateItems = [];
1083         for(var id in widgetRegistry.acqii) {
1084             var reg = widgetRegistry.acqii[id];
1085             var item = reg._object;
1086             if(item.ischanged() || item.isnew() || item.isdeleted()) {
1087                 updateItems.push(item);
1088                 if(item.isnew()) item.id(null);
1089                 for(var field in reg) {
1090                     if(field != '_object')
1091                         item[field]( reg[field].getFormattedValue() );
1092                 }
1093                 
1094                 unflesh.call(item, 'purchase_order');
1095
1096             }
1097         }
1098
1099         // Prepare any line items
1100         var updateEntries = [];
1101         for(var id in widgetRegistry.acqie) {
1102             var reg = widgetRegistry.acqie[id];
1103             var entry = reg._object;
1104             if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
1105                 updateEntries.push(entry);
1106                 if(entry.isnew()) entry.id(null);
1107
1108                 for(var field in reg) {
1109                     if(field != '_object')
1110                         entry[field]( reg[field].getFormattedValue() );
1111                 }
1112                 
1113                 unflesh.call(entry, 'purchase_order', 'lineitem');
1114             }
1115         }
1116     }
1117
1118     progressDialog.show(true);
1119     fieldmapper.standardRequest(
1120         ['open-ils.acq', 'open-ils.acq.invoice.update'],
1121         {
1122             params : [openils.User.authtoken,
1123                 invoice, updateEntries, updateItems, finalizePos],
1124             oncomplete : function(r) {
1125                 progressDialog.hide();
1126                 var invoice = openils.Util.readResponse(r);
1127                 if(invoice) {
1128                     if(args.prorate)
1129                         return prorateInvoice(invoice);
1130                     if (args.clear) {
1131                         location.href = oilsBasePath + '/acq/invoice/view?create=1';
1132                     } else {
1133                         location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
1134                     }
1135                 }
1136             }
1137         }
1138     );
1139 }
1140
1141 function prorateInvoice(invoice) {
1142     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
1143     progressDialog.show(true);
1144
1145     fieldmapper.standardRequest(
1146         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
1147         {
1148             params : [openils.User.authtoken, invoice.id()],
1149             oncomplete : function(r) {
1150                 progressDialog.hide();
1151                 var invoice = openils.Util.readResponse(r);
1152                 if(invoice) {
1153                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
1154                 }
1155             }
1156         }
1157     );
1158 }
1159
1160 function storeExtraCopies(entry, numExtra) {
1161
1162     dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
1163         dojo.string.substitute(
1164             localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
1165
1166     var addCopyHandler;
1167     addCopyHandler = dojo.connect(
1168         extraCopiesGo, 
1169         'onClick',
1170         function() {
1171             extraCopies[entry.lineitem().id()] = {
1172                 numExtra : numExtra, 
1173                 fund : extraCopiesFund.widget.attr('value')
1174             }
1175             extraItemsDialog.hide();
1176             dojo.disconnect(addCopyHandler);
1177         }
1178     );
1179
1180     dojo.connect(
1181         extraCopiesCancel, 
1182         'onClick',
1183         function() { 
1184             widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
1185             extraItemsDialog.hide() 
1186         }
1187     );
1188
1189     extraItemsDialog.show();
1190 }
1191
1192 function validateInvIdent(inv_ident, provider, receiver) {
1193     if (!(inv_ident && provider && receiver)) {
1194         console.info("not enough information to pre-validate inv_ident");
1195         return;
1196     }
1197
1198     openils.Util.show("ident-validation-spinner", "inline");
1199     var pcrud = new openils.PermaCrud();
1200     pcrud.search(
1201         "acqinv", {"inv_ident": inv_ident, "provider": provider}, {
1202             "oncomplete": function(r) {
1203                 openils.Util.hide("ident-validation-spinner");
1204
1205                 /* This could throw an event about the user not having perms,
1206                  * but in such a case the whole interface is already busted
1207                  * anyway. */
1208                 r = openils.Util.readResponse(r);
1209
1210                 var w = invoicePane.getFieldWidget("inv_ident").widget;
1211                 if (r.length) {
1212                     alert(localeStrings.INVOICE_IDENT_COLLIDE);
1213                     w.validator = function() { return false; };
1214                     w.validate();
1215                 } else {
1216                     w.validator = function() { return true; };
1217                     w.validate();
1218                 }
1219                 w.focus();
1220                 pcrud.disconnect();
1221             }
1222         }
1223     );
1224 }
1225
1226 function drawInvoicePane(parentNode, inv, args) {
1227     args = args || {};
1228     var pane;
1229
1230     var override = {};
1231     if(!inv) {
1232         override = {
1233             recv_date : {widgetValue : dojo.date.stamp.toISOString(new Date())},
1234             //receiver : {widgetValue : openils.User.user.ws_ou()},
1235             receiver : {
1236                 "dijitArgs": {
1237                     "onChange": function(val) {
1238                         validateInvIdent(
1239                             invoicePane && invoicePane.getFieldValue("inv_ident"),
1240                             invoicePane && invoicePane.getFieldValue("provider"),
1241                             val
1242                         );
1243                     }
1244                 }
1245             },
1246             recv_method : {widgetValue : 'PPR'}
1247         };
1248     }
1249
1250     dojo.mixin(override, {
1251         provider : { 
1252             dijitArgs : { 
1253                 store_options : { base_filter : { active :"t" } },
1254                 onChange : function(val) {
1255                     pane.setFieldValue('shipper', val);
1256                     validateInvIdent(
1257                         invoicePane && invoicePane.getFieldValue("inv_ident"),
1258                         val,
1259                         invoicePane && invoicePane.getFieldValue("receiver")
1260                     );
1261                 }
1262             } 
1263         },
1264         shipper  : { dijitArgs : { store_options : { base_filter : { active :"t" } } } }
1265     });
1266
1267     for(var field in args) {
1268         override[field] = {widgetValue : args[field]};
1269     }
1270
1271     // push the name of the invoice into the name display field after update
1272     override.inv_ident = dojo.mixin(
1273         override.inv_ident,
1274         {dijitArgs : {onChange :
1275             function(newVal) {
1276                 validateInvIdent(
1277                     newVal,
1278                     invoicePane && invoicePane.getFieldValue("provider"),
1279                     invoicePane && invoicePane.getFieldValue("receiver")
1280                 );
1281
1282                 if (dojo.byId('acq-invoice-summary-name'))
1283                     dojo.byId('acq-invoice-summary-name').innerHTML = newVal;
1284             }
1285         }}
1286     );
1287
1288
1289     pane = new openils.widget.EditPane({
1290         fmObject : inv,
1291         paneStackCount : 2,
1292         fmClass : 'acqinv',
1293         mode : (inv) ? 'edit' : 'create',
1294         hideActionButtons : true,
1295         overrideWidgetArgs : override,
1296         readOnly : (inv) && openils.Util.isTrue(inv.complete()),
1297         requiredFields : [
1298             'inv_ident', 
1299             'recv_date', 
1300             'provider', 
1301             'shipper'
1302         ],
1303         fieldOrder : [
1304             'inv_ident', 
1305             'recv_date', 
1306             'recv_method', 
1307             'inv_type', 
1308             'provider', 
1309             'shipper'
1310         ],
1311         suppressFields : ['id', 'complete']
1312     });
1313
1314     pane.startup();
1315     parentNode.appendChild(pane.domNode);
1316     return pane;
1317 }
1318
1319
1320 function createExtraCopies(oncomplete) {
1321
1322     var lids = [];
1323     for(var liId in extraCopies) {
1324         var data = extraCopies[liId];
1325         for(var i = 0; i < data.numExtra; i++) {
1326             var lid = new fieldmapper.acqlid();
1327             lid.isnew(true);
1328             lid.lineitem(liId);
1329             lid.fund(data.fund);
1330             lid.recv_time('now');
1331             lids.push(lid);
1332         }
1333     }
1334
1335     if(lids.length == 0) 
1336         return oncomplete();
1337
1338     fieldmapper.standardRequest(
1339         ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
1340         {
1341             params : [openils.User.authtoken, lids, true],
1342             oncomplete : function(r) {
1343                 if(openils.Util.readResponse(r))
1344                     oncomplete();
1345             }
1346         }
1347     );
1348
1349 }
1350
1351 function smartSearchSubmitter() {
1352     performSearch(0, !dojo.byId('acq-unified-build-progressively').checked);
1353 }
1354
1355
1356 openils.Util.addOnLoad(init);
1357
1358