]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
ACQ invoice formatting improvements
[working/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
17 dojo.requireLocalization('openils.acq', 'acq');
18 var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
19
20 var fundLabelFormat = ['${0} (${1})', 'code', 'year'];
21 var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
22
23 var cgi = new openils.CGI();
24 var pcrud = new openils.PermaCrud();
25 var attachLi;
26 var attachPo;
27 var invoice;
28 var itemTbody;
29 var itemTemplate;
30 var entryTemplate;
31 var totalInvoicedBox;
32 var totalPaidBox;
33 var balanceOwedBox;
34 var invoicePane;
35 var itemTypes;
36 var virtualId = -1;
37 var extraCopies = {};
38 var extraCopiesFund;
39 var widgetRegistry = {acqie : {}, acqii : {}};
40 var focusLineitem;
41 var searchInitDone = false;
42 var termManager;
43 var resultManager;
44
45 function nodeByName(name, context) {
46     return dojo.query('[name='+name+']', context)[0];
47 }
48
49 function init() {
50
51     attachLi = cgi.param('attach_li') || [];
52     if (!dojo.isArray(attachLi)) 
53         attachLi = [attachLi];
54
55     attachPo = cgi.param('attach_po') || [];
56     if (!dojo.isArray(attachPo)) 
57         attachPo = [attachPo];
58
59     focusLineitem = new openils.CGI().param('focus_li');
60
61     totalInvoicedBox = dojo.byId('acq-total-invoiced-box');
62     totalPaidBox = dojo.byId('acq-total-paid-box');
63     balanceOwedBox = dojo.byId('acq-total-balance-box');
64
65     itemTypes = pcrud.retrieveAll('aiit');
66
67     dojo.byId('acq-invoice-summary-toggle-off').onclick = function() {
68         openils.Util.hide(dojo.byId('acq-invoice-summary'));
69         openils.Util.show(dojo.byId('acq-invoice-summary-small'));
70     };
71
72     dojo.byId('acq-invoice-summary-toggle-on').onclick = function() {
73         openils.Util.show(dojo.byId('acq-invoice-summary'));
74         openils.Util.hide(dojo.byId('acq-invoice-summary-small'));
75     }
76
77     if(cgi.param('create')) {
78         renderInvoice();
79
80         // show summary info by default for new invoices
81         dojo.byId('acq-invoice-summary-toggle-on').onclick();
82
83     } else {
84         dojo.byId('acq-invoice-summary-toggle-off').onclick();
85         fieldmapper.standardRequest(
86             ['open-ils.acq', 'open-ils.acq.invoice.retrieve.authoritative'],
87             {
88                 params : [openils.User.authtoken, invoiceId],
89                 oncomplete : function(r) {
90                     invoice = openils.Util.readResponse(r);     
91                     renderInvoice();
92                 }
93             }
94         );
95     }
96
97     extraCopiesFund = new openils.widget.AutoFieldWidget({
98         fmField : 'fund',
99         fmClass : 'acqlid',
100         searchFilter : {active : 't'},
101         labelFormat : fundLabelFormat,
102         searchFormat : fundSearchFormat,
103         dijitArgs : {required : true},
104         parentNode : dojo.byId('acq-invoice-extra-copies-fund')
105     });
106     extraCopiesFund.build();
107 }
108
109 function renderInvoice() {
110
111     // in create mode, let the LI or PO render the invoice with seed data
112     if( !(cgi.param('create') && (attachPo.length || attachLi.length)) ) {
113         invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), invoice);
114     }
115
116     dojo.byId('acq-invoice-new-item').onclick = function() {
117         var item = new fieldmapper.acqii();
118         item.id(virtualId--);
119         item.isnew(true);
120         addInvoiceItem(item);
121     }
122
123     updateTotalCost();
124
125     if(invoice && openils.Util.isTrue(invoice.complete())) {
126
127         dojo.forEach( // hide widgets that should not be visible for a completed invoice
128             dojo.query('.hide-complete'), 
129             function(node) { openils.Util.hide(node); }
130         );
131
132         new openils.User().getPermOrgList(
133             'ACQ_INVOICE_REOPEN', 
134             function (orgs) {
135                 if(orgs.indexOf(invoice.receiver()) >= 0)
136                     openils.Util.show('acq-invoice-reopen-button-wrapper', 'inline');
137             }, 
138             true, 
139             true
140         );
141     }
142
143     // display items and entries in ID order 
144     // which effectively equates to add order.
145     function idsort(a, b) { return a.id() < b.id() ? -1 : 1 }
146
147     if(invoice) {
148         dojo.forEach(
149             invoice.items().sort(idsort),
150             function(item) {
151                 addInvoiceItem(item);
152             }
153         );
154
155         dojo.forEach(
156             invoice.entries().sort(idsort),
157             function(entry) {
158                 addInvoiceEntry(entry);
159             }
160         );
161     }
162
163     if(attachLi.length) doAttachLi();
164     if(attachPo.length) doAttachPo(0);
165 }
166
167 function doAttachLi(skipInit) {
168
169     //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; 
170     if(cgi.param('create') && !skipInit) {
171
172         // use the first LI in the list to determine the default provider
173         fieldmapper.standardRequest(
174             ['open-ils.acq', 'open-ils.acq.lineitem.retrieve.authoritative'],
175             {
176                 params : [openils.User.authtoken, attachLi[0], {clear_marc:1}],
177                 oncomplete : function(r) {
178                     var li = openils.Util.readResponse(r);
179                     invoicePane = drawInvoicePane(
180                         dojo.byId('acq-view-invoice-div'), null, 
181                         {provider : li.provider(), shipper : li.provider()}
182                     );
183                 }
184             }
185         );
186     }
187
188     dojo.forEach(attachLi,
189         function(li) {
190             var entry = new fieldmapper.acqie();
191             entry.id(virtualId--);
192             entry.isnew(true);
193             entry.lineitem(li);
194             addInvoiceEntry(entry);
195         }
196     );
197 }
198
199 function doAttachPo(idx) {
200
201     if (idx == attachPo.length) return;
202     var poId = attachPo[idx];
203
204     fieldmapper.standardRequest(
205         ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
206         {   async: true,
207             params: [
208                 openils.User.authtoken, poId,
209                 {flesh_lineitem_ids : true, flesh_po_items : true}
210             ],
211             oncomplete: function(r) {
212                 var po = openils.Util.readResponse(r);
213
214                 if(cgi.param('create') && idx == 0) {
215                     // render the invoice using some seed data from the first PO
216                     var invoiceArgs = {provider : po.provider(), shipper : po.provider()}; 
217                     invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
218                 }
219
220                 dojo.forEach(po.lineitems(), 
221                     function(lineitem) {
222                         var entry = new fieldmapper.acqie();
223                         entry.id(virtualId--);
224                         entry.isnew(true);
225                         entry.lineitem(lineitem);
226                         entry.purchase_order(po);
227                         addInvoiceEntry(entry);
228                     }
229                 );
230
231                 dojo.forEach(po.po_items(),
232                     function(poItem) {
233                         var item = new fieldmapper.acqii();
234                         item.id(virtualId--);
235                         item.isnew(true);
236                         item.fund(poItem.fund());
237                         item.title(poItem.title());
238                         item.author(poItem.author());
239                         item.note(poItem.note());
240                         item.inv_item_type(poItem.inv_item_type());
241                         item.purchase_order(po);
242                         item.po_item(poItem);
243                         addInvoiceItem(item);
244                     }
245                 );
246
247                 doAttachPo(++idx);
248             }
249         }
250     );
251 }
252
253 function performSearch(pageDir) {
254     clearSearchResTable(); 
255     var searchObject = termManager.buildSearchObject();
256     dojo.cookie('invs', base64Encode(searchObject));
257     dojo.cookie('invc', dojo.byId("acq-unified-conjunction").getValue());
258
259     if (pageDir == 0) { // new search
260         resultsLoader.displayOffset = 0;
261     } else {
262         resultsLoader.displayOffset += pageDir * resultsLoader.displayLimit;
263     }
264
265     if (resultsLoader.displayOffset == 0) {
266         openils.Util.hide('acq-inv-search-prev');
267     } else {
268         openils.Util.show('acq-inv-search-prev', 'inline');
269     }
270
271     if (dojo.byId('acq-invoice-search-limit-invoiceable').checked) {
272         if (!searchObject.jub) 
273             searchObject.jub = [];
274
275         // exclude lineitems that are "cancelled" (sidebar: 'Mericans spell it 'canceled')
276         searchObject.jub.push({state : 'cancelled', '__not' : true});
277
278         // exclude lineitems already linked to this invoice
279         if (invoice && invoice.id() > 0) { 
280             if (!searchObject.acqinv)
281                 searchObject.acqinv = [];
282             searchObject.acqinv.push({id : invoice.id(), '__not' : true});
283         }
284
285         // limit to lineitems that have invoiceable copies
286         searchObject.acqlisumi = [{item_count : 1, '_gte' : true}];
287
288         // limit to provider if a provider is selected
289         var provider = invoicePane.getFieldValue('provider');
290         if (provider) {
291             if (!searchObject.jub.filter(function(i) { return i.provider != null }).length)
292                 searchObject.jub.push({provider : provider});
293         }
294     }
295
296     if (dojo.byId('acq-invoice-search-sort-title').checked) {
297         uriManager.order_by = 
298             [ {"class": "acqlia", "field":"attr_value", "transform":"first"} ];
299     }
300
301     resultsLoader.lastSearch = searchObject;
302     resultManager.go(searchObject)
303     console.log('Lineitem Search: ' + js2JSON(searchObject));
304     focusLastSearchInput();
305 }
306
307
308 function renderUnifiedSearch() {
309
310     if (!searchInitDone) {
311
312         searchInitDone = true;
313         termManager = new TermManager();
314         resultManager = new ResultManager();
315         resultsLoader = new searchResultsLoader();
316         uriManager = new URIManager();
317
318         // define custom lineitem result handler
319         resultManager.result_types = {
320             "lineitem": {
321                 "search_options": { "id_list": true },
322                 "revealer": function() { },
323                 "finisher": function() {
324                     resultsLoader.batch_length = resultManager.count_results;
325                 },
326                 "adder": function(li) {
327                     resultsLoader.addLineitem(li);
328                 },
329                 "interface": resultsLoader
330             },
331             "no_results": {
332                 "revealer": function() { }
333             }
334         };
335
336         var searchObject = dojo.cookie('invs');
337         console.log('loaded ' + searchObject);
338         if (searchObject) {
339             // if there is a search object cookie, populate the search form
340             termManager.reflect(base64Decode(searchObject));
341             dojo.byId("acq-unified-conjunction").setValue(dojo.cookie('invc'));
342         } else {
343             console.log('adding row');
344             termManager.addRow();
345         }
346     }
347
348     dojo.addClass(dojo.byId('oils-acq-invoice-table'), 'hidden');
349     dojo.removeClass(dojo.byId('oils-acq-invoice-search'), 'hidden');
350     focusLastSearchInput();
351 }
352
353 function focusLastSearchInput() {
354     // TODO: see about making this better and moving it into search/unified.js
355     var wnodes = dojo.query('[name=widget]');
356     var inputNode = wnodes.item(wnodes.length - 1).firstChild;
357     if (inputNode) {
358         try {
359             inputNode.select();
360         } catch(E) {
361             inputNode.focus();
362         }
363     }
364 }
365
366 var resultsTbody, resultsRow;
367 function searchResultsLoader() {
368     this.displayOffset = 0;
369     this.displayLimit = 10;
370
371     if (!resultsTbody) {
372         resultsTbody = dojo.byId('acq-invoice-search-results-tbody');
373         resultsRow = resultsTbody.removeChild(dojo.byId('acq-invoice-search-results-tr'));
374     }
375
376     this.addLineitem = function(li_id) {
377         console.log('Adding search result lineitem ' + li_id);
378         var row = resultsRow.cloneNode(true);
379         resultsTbody.appendChild(row);
380         var checkbox = dojo.query('[name=search-results-checkbox]', row)[0];
381         checkbox.setAttribute('lineitem', li_id);
382
383         // this lineitem is already part of the invoice
384         if (dojo.query('[entry_lineitem_row=' + li_id + ']')[0]) {
385             checkbox.disabled = true;
386             dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced');
387         }
388
389         openils.acq.Lineitem.fetchAndRender(
390             li_id, {}, 
391             function(li, html) { 
392                 dojo.query('[name=search-results-content-div]', row)[0].innerHTML = html;
393             }
394         );
395     }
396 }
397
398 function addSelectedToInvoice() {
399     var inputs = dojo.query('[name=search-results-checkbox]');
400     attachLi = [];
401     dojo.forEach(inputs,
402         function(checkbox) {
403             if (checkbox.checked) {
404                 attachLi.push(checkbox.getAttribute('lineitem'));
405                 checkbox.disabled = true;
406                 checkbox.checked = false;
407                 dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced');
408             }
409         }
410     );
411     doAttachLi(true);
412 }
413
414 function clearSearchResTable() {
415     while (resultsTbody.childNodes[0])
416         resultsTbody.removeChild(resultsTbody.childNodes[0]);
417 }
418
419 function updateTotalCost() {
420
421     var totalCost = 0;    
422     for(var id in widgetRegistry.acqii) 
423         if(!widgetRegistry.acqii[id]._object.isdeleted())
424             totalCost += Number(widgetRegistry.acqii[id].cost_billed.getFormattedValue());
425     for(var id in widgetRegistry.acqie) 
426         if(!widgetRegistry.acqie[id]._object.isdeleted())
427             totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
428     totalInvoicedBox.innerHTML = totalCost.toFixed(2);
429
430     totalPaid = 0;    
431     for(var id in widgetRegistry.acqii) 
432         if(!widgetRegistry.acqii[id]._object.isdeleted())
433             totalPaid += Number(widgetRegistry.acqii[id].amount_paid.getFormattedValue());
434     for(var id in widgetRegistry.acqie) 
435         if(!widgetRegistry.acqie[id]._object.isdeleted())
436             totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
437     totalPaidBox.innerHTML = totalPaid.toFixed(2);
438
439     var buttonsDisabled = false;
440
441     if(totalPaid > totalCost || totalPaid < 0) {
442         openils.Util.addCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
443         invoiceSaveButton.attr('disabled', true);
444         invoiceProrateButton.attr('disabled', true);
445         buttonsDisabled = true;
446     } else {
447         openils.Util.removeCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
448         invoiceSaveButton.attr('disabled', false);
449         invoiceProrateButton.attr('disabled', false);
450     }
451
452     if(totalCost < 0) {
453         openils.Util.addCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
454         invoiceSaveButton.attr('disabled', true);
455         invoiceProrateButton.attr('disabled', true);
456     } else {
457         openils.Util.removeCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
458         if(!buttonsDisabled) {
459             invoiceSaveButton.attr('disabled', false);
460             invoiceProrateButton.attr('disabled', false);
461         }
462     }
463
464     if(totalPaid == totalCost) { // XXX: too rigid?
465         invoiceCloseButton.attr('disabled', false);
466     } else {
467         invoiceCloseButton.attr('disabled', true);
468     }
469
470     balanceOwedBox.innerHTML = (totalCost - totalPaid).toFixed(2);
471
472     updateExpectedCost();
473 }
474
475
476 function registerWidget(obj, field, widget, callback) {
477     var blob = widgetRegistry[obj.classname];
478     if(!blob[obj.id()]) 
479         blob[obj.id()] = {_object : obj};
480     blob[obj.id()][field] = widget;
481     widget.build(
482         function(w, ww) {
483             dojo.connect(w, 'onChange', 
484                 function(newVal) { 
485                     obj.ischanged(true); 
486                     updateTotalCost();
487                 }
488             );
489             if(callback) callback(w, ww);
490         }
491     );
492     return widget;
493 }
494
495 function addInvoiceItem(item) {
496     itemTbody = dojo.byId('acq-invoice-item-tbody');
497     if(itemTemplate == null) {
498         itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
499     }
500
501     var row = itemTemplate.cloneNode(true);
502     var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
503
504     dojo.forEach(
505         ['title', 'author', 'cost_billed', 'amount_paid'], 
506         function(field) {
507             
508             var args;
509             if(field == 'title' || field == 'author') {
510                 //args = {style : 'width:10em'};
511             } else if(field == 'cost_billed' || field == 'amount_paid') {
512                 args = {required : true, style : 'width: 8em'};
513             }
514
515             registerWidget(
516                 item,
517                 field,
518                 new openils.widget.AutoFieldWidget({
519                     fmClass : 'acqii',
520                     fmObject : item,
521                     fmField : field,
522                     readOnly : invoice && openils.Util.isTrue(invoice.complete()),
523                     dijitArgs : args,
524                     parentNode : nodeByName(field, row)
525                 })
526             )
527         }
528     );
529
530
531     /* ----------- fund -------------- */
532     var fundArgs = {
533         fmClass : 'acqii',
534         fmObject : item,
535         fmField : 'fund',
536         labelFormat : fundLabelFormat,
537         searchFormat : fundSearchFormat,
538         readOnly : invoice && openils.Util.isTrue(invoice.complete()),
539         dijitArgs : {required : true},
540         parentNode : nodeByName('fund', row)
541     }
542
543     if(item.fund_debit()) {
544         fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
545     } else {
546         fundArgs.searchFilter = {active : 't'}
547         if(itemType && openils.Util.isTrue(itemType.prorate()))
548             fundArgs.dijitArgs = {disabled : true};
549     }
550
551     var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
552     registerWidget(item, 'fund', fundWidget);
553
554     /* ---------- inv_item_type ------------- */
555
556     if(item.po_item()) {
557
558         // read-only item view for items that were the result of a po-item
559         var po = item.purchase_order();
560         var po_item = item.po_item();
561         var node = nodeByName('inv_item_type', row);
562         var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
563         orderDate = (!po.order_date()) ? '' : 
564                 dojo.date.locale.format(dojo.date.stamp.fromISOString(po.order_date()), {selector:'date'});
565
566         node.innerHTML = dojo.string.substitute(
567             localeStrings.INVOICE_ITEM_PO_DETAILS, 
568             [ 
569                 itemType.name(),
570                 oilsBasePath, 
571                 po.id(), 
572                 po.name(), 
573                 orderDate,
574                 po_item.estimated_cost() 
575             ]
576         );
577
578     } else {
579
580         registerWidget(
581             item,
582             'inv_item_type',
583             new openils.widget.AutoFieldWidget({
584                 fmObject : item,
585                 fmField : 'inv_item_type',
586                 parentNode : nodeByName('inv_item_type', row),
587                 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
588                 dijitArgs : {required : true}
589             }),
590             function(w, ww) {
591                 // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
592                 // since this charge will be prorated against (potentially) multiple funds
593                 dojo.connect(w, 'onChange', 
594                     function() {
595                         if(!item.fund_debit()) {
596                             var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
597                             if(!itemType) return;
598                             if(openils.Util.isTrue(itemType.prorate())) {
599                                 fundWidget.widget.attr('disabled', true);
600                                 fundWidget.widget.attr('value', '');
601                             } else {
602                                 fundWidget.widget.attr('disabled', false);
603                             }
604                         }
605                     }
606                 );
607             }
608         );
609     }
610
611     nodeByName('delete', row).onclick = function() {
612         var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
613         var msg = dojo.string.substitute(
614             localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
615                 cost || 0,
616                 widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue() || ''
617             ]
618         );
619         if(!confirm(msg)) return;
620         itemTbody.removeChild(row);
621         item.isdeleted(true);
622         if(item.isnew())
623             delete widgetRegistry.acqii[item.id()];
624         updateTotalCost();
625     }
626
627     itemTbody.appendChild(row);
628     updateTotalCost();
629 }
630
631 function updateReceiveLink(li) {
632     if (!invoiceId)
633         return; /* can't do this with unsaved invoices */
634
635     var link = dojo.byId("acq-view-invoice-receive-link");
636     if (link.onclick) return; /* only need to do this once */
637
638     /* don't do this if there's nothing receivable on the lineitem */
639     if (li.order_summary().recv_count() + li.order_summary().cancel_count() >=
640         li.order_summary().item_count())
641         return;
642
643     openils.Util.show("acq-view-invoice-receive");
644     link.onclick = function() { location.href =  oilsBasePath + '/acq/invoice/receive/' + invoiceId; };
645 }
646
647 /*
648  * Ensures focusLineitem is in view and causes a brief 
649  * border around the lineitem to come to life then fade.
650  */
651 function focusLi() {
652     if (!focusLineitem) return;
653
654     // set during addLineitem()
655     var node = dojo.byId('li-title-ref-' + focusLineitem);
656
657     console.log('focus: li-title-ref-' + focusLineitem + ' : ' + node);
658
659     // LI may not yet be rendered
660     if (!node) return; 
661
662     console.log('focusing ' + focusLineitem);
663
664     // prevent numerous re-focuses
665     focusLineitem = null; 
666
667     // causes the full row to be visible
668     dijit.scrollIntoView(node);
669
670     dojo.require('dojox.fx');
671
672     setTimeout(
673         function() {
674             dojox.fx.highlight({color : '#BB4433', node : node, duration : 2000}).play();
675         }, 
676     100);
677 }
678
679
680 // expected cost is totalCostInvoiced + totalCostNotYetInvoiced
681 function updateExpectedCost() {
682
683     var cost = Number(totalInvoicedBox.innerHTML || 0);
684
685     // for any LI's that are not yet billed (i.e. filled in)
686     // use the total expected cost for that lineitem.
687     for(var id in widgetRegistry.acqie) {
688         var entry = widgetRegistry.acqie[id]._object;
689         if(!entry.isdeleted()) {
690             if (Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue()) == 0) {
691                 var li = entry.lineitem();
692                 cost += 
693                     Number(li.order_summary().estimated_amount()) - 
694                     Number(li.order_summary().paid_amount());
695             }
696         }
697     }
698
699     dojo.byId('acq-invoice-summary-cost').innerHTML = cost.toFixed(2);
700 }
701
702 var invoicEntryWidgets = {};
703 function addInvoiceEntry(entry) {
704     console.log('Adding new entry for lineitem ' + entry.lineitem());
705
706     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
707     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
708     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
709
710     dojo.byId('acq-invoice-summary-count').innerHTML = 
711         Number(dojo.byId('acq-invoice-summary-count').innerHTML) + 1;
712
713     entryTbody = dojo.byId('acq-invoice-entry-tbody');
714     if(entryTemplate == null) {
715         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
716     }
717
718     if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
719         // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
720         return;
721
722     var row = entryTemplate.cloneNode(true);
723     row.setAttribute('lineitem', entry.lineitem());
724     row.setAttribute('entry_lineitem_row', entry.lineitem());
725
726     openils.acq.Lineitem.fetchAndRender(
727         entry.lineitem(), {}, 
728         function(li, html) { 
729             entry.lineitem(li);
730             entry.purchase_order(li.purchase_order());
731             nodeByName('title_details', row).innerHTML = html;
732
733             nodeByName('title_details', row).parentNode.id = 'li-title-ref-' + li.id();
734             console.log(dojo.byId('li-title-ref-' + li.id()));
735
736             updateReceiveLink(li);
737
738             // set some default values if otherwise unset
739             if (!invoicePane.getFieldValue('receiver')) {
740                 invoicePane.setFieldValue('receiver', li.purchase_order().ordering_agency());
741             }
742             if (!invoicePane.getFieldValue('provider')) {
743                 invoicePane.setFieldValue('provider', li.purchase_order().provider());
744             }
745
746             dojo.forEach(
747                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
748                 function(field) {
749                     var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
750                     if(field.match(/count/)) {
751                         dijitArgs.style = 'width:4em;';
752                     } else {
753                         dijitArgs.style = 'width:9em;';
754                     }
755                     if(entry.isnew() && field == 'phys_item_count') {
756                         // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
757                         var count = Number(li.order_summary().item_count() || 0) - 
758                                     Number(li.order_summary().cancel_count() || 0) -
759                                     Number(li.order_summary().invoice_count() || 0);
760                         if(count < 0) count = 0;
761                         dijitArgs.value = count;
762                     }
763                     registerWidget(
764                         entry, 
765                         field,
766                         new openils.widget.AutoFieldWidget({
767                             fmObject : entry,
768                             fmClass : 'acqie',
769                             fmField : field,
770                             dijitArgs : dijitArgs,
771                             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
772                             parentNode : nodeByName(field, row)
773                         }),
774                         function(w) {    
775
776                             if(field == 'phys_item_count') {
777                                 dojo.connect(w, 'onChange', 
778                                     function() {
779                                         // staff entered a higher number in the receive field than was originally ordered
780                                         // taking into account already invoiced items
781                                         var extra = Number(this.attr('value')) - 
782                                             (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
783                                         if(extra > 0) {
784                                             storeExtraCopies(entry, extra);
785                                         }
786                                     }
787                                 )
788                             } // if
789
790                             if(field == 'inv_item_count' || field == 'cost_billed') {
791                                 setPerCopyPrice(row, entry);
792                                 // update the per-copy count as invoice count and cost billed change 
793                                 dojo.connect(w, 'onChange', function() { setPerCopyPrice(row, entry) } );
794                             } 
795
796                         } // func
797                     );
798                 }
799             );
800
801             updateTotalCost();
802             if (focusLineitem == li.id())
803                 focusLi();
804         }
805     );
806
807     nodeByName('detach', row).onclick = function() {
808         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
809         var idents = [];
810         dojo.forEach(['isbn', 'upc', 'issn'], 
811             function(ident) { 
812                 var val = liMarcAttr(entry.lineitem(), ident);
813                 if(val) idents.push(val); 
814             }
815         );
816
817         var msg = dojo.string.substitute(
818             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
819                 cost || 0,
820                 liMarcAttr(entry.lineitem(), 'title'),
821                 liMarcAttr(entry.lineitem(), 'author'),
822                 idents.join(',')
823             ]
824         );
825         if(!confirm(msg)) return;
826         entryTbody.removeChild(row);
827         entry.isdeleted(true);
828         if(entry.isnew())
829             delete widgetRegistry.acqie[entry.id()];
830         updateTotalCost();
831     }
832
833     entryTbody.appendChild(row);
834 }
835
836 function setPerCopyPrice(row, entry) {
837     var inv_w = widgetRegistry.acqie[entry.id()].inv_item_count;
838     var bill_w = widgetRegistry.acqie[entry.id()].cost_billed;
839
840     if (inv_w && bill_w) {
841         var invoiced = Number(inv_w.getFormattedValue());
842         var billed = Number(bill_w.getFormattedValue());
843         console.log(invoiced + ' : ' + billed);
844         if (invoiced > 0) {
845             nodeByName('amount_paid_per_copy', row).innerHTML = (billed / invoiced).toFixed(2);
846         } else {
847             nodeByName('amount_paid_per_copy', row).innerHTML = '0.00';
848         }
849     }
850 }
851
852 function liMarcAttr(lineitem, name) {
853     var attr = lineitem.attributes().filter(
854         function(attr) { 
855             if(
856                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
857                 attr.attr_name() == name) 
858                     return attr 
859         } 
860     )[0];
861     return (attr) ? attr.attr_value() : '';
862 }
863
864 function saveChanges(args) {
865     args = args || {};
866     createExtraCopies(function() { saveChangesPartTwo(args); });
867 }
868
869 // Define a helper function to 'unflesh' sub-objects from an fmclass object.
870 // 'this' specifies the object; the arguments specify a list of names of
871 // sub-objects.
872 function unflesh() {
873     var _, $ = this;
874     dojo.forEach(arguments, function (n) {
875         _ = $[n]();
876         if (_ !== null && typeof _ === 'object')
877             $[n]( _.id() );
878     });
879 }
880
881 function saveChangesPartTwo(args) {
882     args = args || {};
883
884     if(args.reopen) {
885         invoice.complete('f');
886
887     } else {
888
889         // Prepare an invoice for submission
890         if(!invoice) {
891             invoice = new fieldmapper.acqinv();
892             invoice.isnew(true);
893         } else {
894             invoice.ischanged(true); // for now, just always update
895         }
896
897         var e = invoicePane.mapValues(function (n, v) { invoice[n](v); });
898         if (e instanceof Error) {
899             alert(e.message);
900             return;
901         }
902
903         if(args.close)
904             invoice.complete('t');
905
906
907         // Prepare any charge items
908         var updateItems = [];
909         for(var id in widgetRegistry.acqii) {
910             var reg = widgetRegistry.acqii[id];
911             var item = reg._object;
912             if(item.ischanged() || item.isnew() || item.isdeleted()) {
913                 updateItems.push(item);
914                 if(item.isnew()) item.id(null);
915                 for(var field in reg) {
916                     if(field != '_object')
917                         item[field]( reg[field].getFormattedValue() );
918                 }
919                 
920                 unflesh.call(item, 'purchase_order');
921
922             }
923         }
924
925         // Prepare any line items
926         var updateEntries = [];
927         for(var id in widgetRegistry.acqie) {
928             var reg = widgetRegistry.acqie[id];
929             var entry = reg._object;
930             if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
931                 updateEntries.push(entry);
932                 if(entry.isnew()) entry.id(null);
933
934                 for(var field in reg) {
935                     if(field != '_object')
936                         entry[field]( reg[field].getFormattedValue() );
937                 }
938                 
939                 unflesh.call(entry, 'purchase_order', 'lineitem');
940             }
941         }
942     }
943
944     progressDialog.show(true);
945     fieldmapper.standardRequest(
946         ['open-ils.acq', 'open-ils.acq.invoice.update'],
947         {
948             params : [openils.User.authtoken, invoice, updateEntries, updateItems],
949             oncomplete : function(r) {
950                 progressDialog.hide();
951                 var invoice = openils.Util.readResponse(r);
952                 if(invoice) {
953                     if(args.prorate)
954                         return prorateInvoice(invoice);
955                     if (args.clear) {
956                         location.href = oilsBasePath + '/acq/invoice/view?create=1';
957                     } else {
958                         location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
959                     }
960                 }
961             }
962         }
963     );
964 }
965
966 function prorateInvoice(invoice) {
967     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
968     progressDialog.show(true);
969
970     fieldmapper.standardRequest(
971         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
972         {
973             params : [openils.User.authtoken, invoice.id()],
974             oncomplete : function(r) {
975                 progressDialog.hide();
976                 var invoice = openils.Util.readResponse(r);
977                 if(invoice) {
978                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
979                 }
980             }
981         }
982     );
983 }
984
985 function storeExtraCopies(entry, numExtra) {
986
987     dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
988         dojo.string.substitute(
989             localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
990
991     var addCopyHandler;
992     addCopyHandler = dojo.connect(
993         extraCopiesGo, 
994         'onClick',
995         function() {
996             extraCopies[entry.lineitem().id()] = {
997                 numExtra : numExtra, 
998                 fund : extraCopiesFund.widget.attr('value')
999             }
1000             extraItemsDialog.hide();
1001             dojo.disconnect(addCopyHandler);
1002         }
1003     );
1004
1005     dojo.connect(
1006         extraCopiesCancel, 
1007         'onClick',
1008         function() { 
1009             widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
1010             extraItemsDialog.hide() 
1011         }
1012     );
1013
1014     extraItemsDialog.show();
1015 }
1016
1017 function createExtraCopies(oncomplete) {
1018
1019     var lids = [];
1020     for(var liId in extraCopies) {
1021         var data = extraCopies[liId];
1022         for(var i = 0; i < data.numExtra; i++) {
1023             var lid = new fieldmapper.acqlid();
1024             lid.isnew(true);
1025             lid.lineitem(liId);
1026             lid.fund(data.fund);
1027             lid.recv_time('now');
1028             lids.push(lid);
1029         }
1030     }
1031
1032     if(lids.length == 0) 
1033         return oncomplete();
1034
1035     fieldmapper.standardRequest(
1036         ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
1037         {
1038             params : [openils.User.authtoken, lids, true],
1039             oncomplete : function(r) {
1040                 if(openils.Util.readResponse(r))
1041                     oncomplete();
1042             }
1043         }
1044     );
1045
1046 }
1047
1048
1049 openils.Util.addOnLoad(init);
1050
1051