]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
ACQ invoice inline lineitem search and add
[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/)) dijitArgs.style = 'width:9em';
751                     if(entry.isnew() && field == 'phys_item_count') {
752                         // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
753                         var count = Number(li.order_summary().item_count() || 0) - 
754                                     Number(li.order_summary().cancel_count() || 0) -
755                                     Number(li.order_summary().invoice_count() || 0);
756                         if(count < 0) count = 0;
757                         dijitArgs.value = count;
758                     }
759                     registerWidget(
760                         entry, 
761                         field,
762                         new openils.widget.AutoFieldWidget({
763                             fmObject : entry,
764                             fmClass : 'acqie',
765                             fmField : field,
766                             dijitArgs : dijitArgs,
767                             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
768                             parentNode : nodeByName(field, row)
769                         }),
770                         function(w) {    
771
772                             if(field == 'phys_item_count') {
773                                 dojo.connect(w, 'onChange', 
774                                     function() {
775                                         // staff entered a higher number in the receive field than was originally ordered
776                                         // taking into account already invoiced items
777                                         var extra = Number(this.attr('value')) - 
778                                             (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
779                                         if(extra > 0) {
780                                             storeExtraCopies(entry, extra);
781                                         }
782                                     }
783                                 )
784                             } // if
785
786                             if(field == 'inv_item_count' || field == 'cost_billed') {
787                                 setPerCopyPrice(row, entry);
788                                 // update the per-copy count as invoice count and cost billed change 
789                                 dojo.connect(w, 'onChange', function() { setPerCopyPrice(row, entry) } );
790                             } 
791
792                         } // func
793                     );
794                 }
795             );
796
797             updateTotalCost();
798             if (focusLineitem == li.id())
799                 focusLi();
800         }
801     );
802
803     nodeByName('detach', row).onclick = function() {
804         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
805         var idents = [];
806         dojo.forEach(['isbn', 'upc', 'issn'], 
807             function(ident) { 
808                 var val = liMarcAttr(entry.lineitem(), ident);
809                 if(val) idents.push(val); 
810             }
811         );
812
813         var msg = dojo.string.substitute(
814             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
815                 cost || 0,
816                 liMarcAttr(entry.lineitem(), 'title'),
817                 liMarcAttr(entry.lineitem(), 'author'),
818                 idents.join(',')
819             ]
820         );
821         if(!confirm(msg)) return;
822         entryTbody.removeChild(row);
823         entry.isdeleted(true);
824         if(entry.isnew())
825             delete widgetRegistry.acqie[entry.id()];
826         updateTotalCost();
827     }
828
829     entryTbody.appendChild(row);
830 }
831
832 function setPerCopyPrice(row, entry) {
833     var inv_w = widgetRegistry.acqie[entry.id()].inv_item_count;
834     var bill_w = widgetRegistry.acqie[entry.id()].cost_billed;
835
836     if (inv_w && bill_w) {
837         var invoiced = Number(inv_w.getFormattedValue());
838         var billed = Number(bill_w.getFormattedValue());
839         console.log(invoiced + ' : ' + billed);
840         if (invoiced > 0) {
841             nodeByName('amount_paid_per_copy', row).innerHTML = (billed / invoiced).toFixed(2);
842         } else {
843             nodeByName('amount_paid_per_copy', row).innerHTML = '0.00';
844         }
845     }
846 }
847
848 function liMarcAttr(lineitem, name) {
849     var attr = lineitem.attributes().filter(
850         function(attr) { 
851             if(
852                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
853                 attr.attr_name() == name) 
854                     return attr 
855         } 
856     )[0];
857     return (attr) ? attr.attr_value() : '';
858 }
859
860 function saveChanges(args) {
861     args = args || {};
862     createExtraCopies(function() { saveChangesPartTwo(args); });
863 }
864
865 // Define a helper function to 'unflesh' sub-objects from an fmclass object.
866 // 'this' specifies the object; the arguments specify a list of names of
867 // sub-objects.
868 function unflesh() {
869     var _, $ = this;
870     dojo.forEach(arguments, function (n) {
871         _ = $[n]();
872         if (_ !== null && typeof _ === 'object')
873             $[n]( _.id() );
874     });
875 }
876
877 function saveChangesPartTwo(args) {
878     args = args || {};
879
880     if(args.reopen) {
881         invoice.complete('f');
882
883     } else {
884
885         // Prepare an invoice for submission
886         if(!invoice) {
887             invoice = new fieldmapper.acqinv();
888             invoice.isnew(true);
889         } else {
890             invoice.ischanged(true); // for now, just always update
891         }
892
893         var e = invoicePane.mapValues(function (n, v) { invoice[n](v); });
894         if (e instanceof Error) {
895             alert(e.message);
896             return;
897         }
898
899         if(args.close)
900             invoice.complete('t');
901
902
903         // Prepare any charge items
904         var updateItems = [];
905         for(var id in widgetRegistry.acqii) {
906             var reg = widgetRegistry.acqii[id];
907             var item = reg._object;
908             if(item.ischanged() || item.isnew() || item.isdeleted()) {
909                 updateItems.push(item);
910                 if(item.isnew()) item.id(null);
911                 for(var field in reg) {
912                     if(field != '_object')
913                         item[field]( reg[field].getFormattedValue() );
914                 }
915                 
916                 unflesh.call(item, 'purchase_order');
917
918             }
919         }
920
921         // Prepare any line items
922         var updateEntries = [];
923         for(var id in widgetRegistry.acqie) {
924             var reg = widgetRegistry.acqie[id];
925             var entry = reg._object;
926             if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
927                 updateEntries.push(entry);
928                 if(entry.isnew()) entry.id(null);
929
930                 for(var field in reg) {
931                     if(field != '_object')
932                         entry[field]( reg[field].getFormattedValue() );
933                 }
934                 
935                 unflesh.call(entry, 'purchase_order', 'lineitem');
936             }
937         }
938     }
939
940     progressDialog.show(true);
941     fieldmapper.standardRequest(
942         ['open-ils.acq', 'open-ils.acq.invoice.update'],
943         {
944             params : [openils.User.authtoken, invoice, updateEntries, updateItems],
945             oncomplete : function(r) {
946                 progressDialog.hide();
947                 var invoice = openils.Util.readResponse(r);
948                 if(invoice) {
949                     if(args.prorate)
950                         return prorateInvoice(invoice);
951                     if (args.clear) {
952                         location.href = oilsBasePath + '/acq/invoice/view?create=1';
953                     } else {
954                         location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
955                     }
956                 }
957             }
958         }
959     );
960 }
961
962 function prorateInvoice(invoice) {
963     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
964     progressDialog.show(true);
965
966     fieldmapper.standardRequest(
967         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
968         {
969             params : [openils.User.authtoken, invoice.id()],
970             oncomplete : function(r) {
971                 progressDialog.hide();
972                 var invoice = openils.Util.readResponse(r);
973                 if(invoice) {
974                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
975                 }
976             }
977         }
978     );
979 }
980
981 function storeExtraCopies(entry, numExtra) {
982
983     dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
984         dojo.string.substitute(
985             localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
986
987     var addCopyHandler;
988     addCopyHandler = dojo.connect(
989         extraCopiesGo, 
990         'onClick',
991         function() {
992             extraCopies[entry.lineitem().id()] = {
993                 numExtra : numExtra, 
994                 fund : extraCopiesFund.widget.attr('value')
995             }
996             extraItemsDialog.hide();
997             dojo.disconnect(addCopyHandler);
998         }
999     );
1000
1001     dojo.connect(
1002         extraCopiesCancel, 
1003         'onClick',
1004         function() { 
1005             widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
1006             extraItemsDialog.hide() 
1007         }
1008     );
1009
1010     extraItemsDialog.show();
1011 }
1012
1013 function createExtraCopies(oncomplete) {
1014
1015     var lids = [];
1016     for(var liId in extraCopies) {
1017         var data = extraCopies[liId];
1018         for(var i = 0; i < data.numExtra; i++) {
1019             var lid = new fieldmapper.acqlid();
1020             lid.isnew(true);
1021             lid.lineitem(liId);
1022             lid.fund(data.fund);
1023             lid.recv_time('now');
1024             lids.push(lid);
1025         }
1026     }
1027
1028     if(lids.length == 0) 
1029         return oncomplete();
1030
1031     fieldmapper.standardRequest(
1032         ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
1033         {
1034             params : [openils.User.authtoken, lids, true],
1035             oncomplete : function(r) {
1036                 if(openils.Util.readResponse(r))
1037                     oncomplete();
1038             }
1039         }
1040     );
1041
1042 }
1043
1044
1045 openils.Util.addOnLoad(init);
1046
1047