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