]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
6c306dba5c30f5aeb263f7beff66ee62a86c78dc
[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 allResSelected = false;
415 function clearSearchResTable() {
416     allResSelected = false;
417     while (resultsTbody.childNodes[0])
418         resultsTbody.removeChild(resultsTbody.childNodes[0]);
419 }
420
421 function selectSearchResults() {
422     allResSelected = !allResSelected;
423     dojo.query('[name=search-results-checkbox]').forEach(
424         function(input) { input.checked = allResSelected });
425 }
426
427 function updateTotalCost() {
428
429     var totalCost = 0;    
430     for(var id in widgetRegistry.acqii) 
431         if(!widgetRegistry.acqii[id]._object.isdeleted())
432             totalCost += Number(widgetRegistry.acqii[id].cost_billed.getFormattedValue());
433     for(var id in widgetRegistry.acqie) 
434         if(!widgetRegistry.acqie[id]._object.isdeleted())
435             totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
436     totalInvoicedBox.innerHTML = totalCost.toFixed(2);
437
438     totalPaid = 0;    
439     for(var id in widgetRegistry.acqii) 
440         if(!widgetRegistry.acqii[id]._object.isdeleted())
441             totalPaid += Number(widgetRegistry.acqii[id].amount_paid.getFormattedValue());
442     for(var id in widgetRegistry.acqie) 
443         if(!widgetRegistry.acqie[id]._object.isdeleted())
444             totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
445     totalPaidBox.innerHTML = totalPaid.toFixed(2);
446
447     var buttonsDisabled = false;
448
449     if(totalPaid > totalCost || totalPaid < 0) {
450         openils.Util.addCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
451         invoiceSaveButton.attr('disabled', true);
452         invoiceProrateButton.attr('disabled', true);
453         buttonsDisabled = true;
454     } else {
455         openils.Util.removeCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
456         invoiceSaveButton.attr('disabled', false);
457         invoiceProrateButton.attr('disabled', false);
458     }
459
460     if(totalCost < 0) {
461         openils.Util.addCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
462         invoiceSaveButton.attr('disabled', true);
463         invoiceProrateButton.attr('disabled', true);
464     } else {
465         openils.Util.removeCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
466         if(!buttonsDisabled) {
467             invoiceSaveButton.attr('disabled', false);
468             invoiceProrateButton.attr('disabled', false);
469         }
470     }
471
472     if(totalPaid == totalCost) { // XXX: too rigid?
473         invoiceCloseButton.attr('disabled', false);
474     } else {
475         invoiceCloseButton.attr('disabled', true);
476     }
477
478     balanceOwedBox.innerHTML = (totalCost - totalPaid).toFixed(2);
479
480     updateExpectedCost();
481 }
482
483
484 function registerWidget(obj, field, widget, callback) {
485     var blob = widgetRegistry[obj.classname];
486     if(!blob[obj.id()]) 
487         blob[obj.id()] = {_object : obj};
488     blob[obj.id()][field] = widget;
489     widget.build(
490         function(w, ww) {
491             dojo.connect(w, 'onChange', 
492                 function(newVal) { 
493                     obj.ischanged(true); 
494                     updateTotalCost();
495                 }
496             );
497             if(callback) callback(w, ww);
498         }
499     );
500     return widget;
501 }
502
503 function addInvoiceItem(item) {
504     itemTbody = dojo.byId('acq-invoice-item-tbody');
505     if(itemTemplate == null) {
506         itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
507     }
508
509     var row = itemTemplate.cloneNode(true);
510     var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
511
512     dojo.forEach(
513         ['title', 'author', 'cost_billed', 'amount_paid'], 
514         function(field) {
515             
516             var args;
517             if(field == 'title' || field == 'author') {
518                 //args = {style : 'width:10em'};
519             } else if(field == 'cost_billed' || field == 'amount_paid') {
520                 args = {required : true, style : 'width: 8em'};
521             }
522
523             registerWidget(
524                 item,
525                 field,
526                 new openils.widget.AutoFieldWidget({
527                     fmClass : 'acqii',
528                     fmObject : item,
529                     fmField : field,
530                     readOnly : invoice && openils.Util.isTrue(invoice.complete()),
531                     dijitArgs : args,
532                     parentNode : nodeByName(field, row)
533                 })
534             )
535         }
536     );
537
538
539     /* ----------- fund -------------- */
540     var fundArgs = {
541         fmClass : 'acqii',
542         fmObject : item,
543         fmField : 'fund',
544         labelFormat : fundLabelFormat,
545         searchFormat : fundSearchFormat,
546         readOnly : invoice && openils.Util.isTrue(invoice.complete()),
547         dijitArgs : {required : true},
548         parentNode : nodeByName('fund', row)
549     }
550
551     if(item.fund_debit()) {
552         fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
553     } else {
554         fundArgs.searchFilter = {active : 't'}
555         if(itemType && openils.Util.isTrue(itemType.prorate()))
556             fundArgs.dijitArgs = {disabled : true};
557     }
558
559     var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
560     registerWidget(item, 'fund', fundWidget);
561
562     /* ---------- inv_item_type ------------- */
563
564     if(item.po_item()) {
565
566         // read-only item view for items that were the result of a po-item
567         var po = item.purchase_order();
568         var po_item = item.po_item();
569         var node = nodeByName('inv_item_type', row);
570         var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
571         orderDate = (!po.order_date()) ? '' : 
572                 dojo.date.locale.format(dojo.date.stamp.fromISOString(po.order_date()), {selector:'date'});
573
574         node.innerHTML = dojo.string.substitute(
575             localeStrings.INVOICE_ITEM_PO_DETAILS, 
576             [ 
577                 itemType.name(),
578                 oilsBasePath, 
579                 po.id(), 
580                 po.name(), 
581                 orderDate,
582                 po_item.estimated_cost() 
583             ]
584         );
585
586     } else {
587
588         registerWidget(
589             item,
590             'inv_item_type',
591             new openils.widget.AutoFieldWidget({
592                 fmObject : item,
593                 fmField : 'inv_item_type',
594                 parentNode : nodeByName('inv_item_type', row),
595                 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
596                 dijitArgs : {required : true}
597             }),
598             function(w, ww) {
599                 // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
600                 // since this charge will be prorated against (potentially) multiple funds
601                 dojo.connect(w, 'onChange', 
602                     function() {
603                         if(!item.fund_debit()) {
604                             var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
605                             if(!itemType) return;
606                             if(openils.Util.isTrue(itemType.prorate())) {
607                                 fundWidget.widget.attr('disabled', true);
608                                 fundWidget.widget.attr('value', '');
609                             } else {
610                                 fundWidget.widget.attr('disabled', false);
611                             }
612                         }
613                     }
614                 );
615             }
616         );
617     }
618
619     nodeByName('delete', row).onclick = function() {
620         var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
621         var msg = dojo.string.substitute(
622             localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
623                 cost || 0,
624                 widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue() || ''
625             ]
626         );
627         if(!confirm(msg)) return;
628         itemTbody.removeChild(row);
629         item.isdeleted(true);
630         if(item.isnew())
631             delete widgetRegistry.acqii[item.id()];
632         updateTotalCost();
633     }
634
635     itemTbody.appendChild(row);
636     updateTotalCost();
637 }
638
639 function updateReceiveLink(li) {
640     if (!invoiceId)
641         return; /* can't do this with unsaved invoices */
642
643     var link = dojo.byId("acq-view-invoice-receive-link");
644     if (link.onclick) return; /* only need to do this once */
645
646     /* don't do this if there's nothing receivable on the lineitem */
647     if (li.order_summary().recv_count() + li.order_summary().cancel_count() >=
648         li.order_summary().item_count())
649         return;
650
651     openils.Util.show("acq-view-invoice-receive");
652     link.onclick = function() { location.href =  oilsBasePath + '/acq/invoice/receive/' + invoiceId; };
653 }
654
655 /*
656  * Ensures focusLineitem is in view and causes a brief 
657  * border around the lineitem to come to life then fade.
658  */
659 function focusLi() {
660     if (!focusLineitem) return;
661
662     // set during addLineitem()
663     var node = dojo.byId('li-title-ref-' + focusLineitem);
664
665     console.log('focus: li-title-ref-' + focusLineitem + ' : ' + node);
666
667     // LI may not yet be rendered
668     if (!node) return; 
669
670     console.log('focusing ' + focusLineitem);
671
672     // prevent numerous re-focuses
673     focusLineitem = null; 
674
675     // causes the full row to be visible
676     dijit.scrollIntoView(node);
677
678     dojo.require('dojox.fx');
679
680     setTimeout(
681         function() {
682             dojox.fx.highlight({color : '#BB4433', node : node, duration : 2000}).play();
683         }, 
684     100);
685 }
686
687
688 // expected cost is totalCostInvoiced + totalCostNotYetInvoiced
689 function updateExpectedCost() {
690
691     var cost = Number(totalInvoicedBox.innerHTML || 0);
692
693     // for any LI's that are not yet billed (i.e. filled in)
694     // use the total expected cost for that lineitem.
695     for(var id in widgetRegistry.acqie) {
696         var entry = widgetRegistry.acqie[id]._object;
697         if(!entry.isdeleted()) {
698             if (Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue()) == 0) {
699                 var li = entry.lineitem();
700                 cost += 
701                     Number(li.order_summary().estimated_amount()) - 
702                     Number(li.order_summary().paid_amount());
703             }
704         }
705     }
706
707     dojo.byId('acq-invoice-summary-cost').innerHTML = cost.toFixed(2);
708 }
709
710 var invoicEntryWidgets = {};
711 function addInvoiceEntry(entry) {
712     console.log('Adding new entry for lineitem ' + entry.lineitem());
713
714     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
715     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
716     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
717
718     dojo.byId('acq-invoice-summary-count').innerHTML = 
719         Number(dojo.byId('acq-invoice-summary-count').innerHTML) + 1;
720
721     entryTbody = dojo.byId('acq-invoice-entry-tbody');
722     if(entryTemplate == null) {
723         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
724     }
725
726     if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
727         // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
728         return;
729
730     var row = entryTemplate.cloneNode(true);
731     row.setAttribute('lineitem', entry.lineitem());
732     row.setAttribute('entry_lineitem_row', entry.lineitem());
733
734     openils.acq.Lineitem.fetchAndRender(
735         entry.lineitem(), {}, 
736         function(li, html) { 
737             entry.lineitem(li);
738             entry.purchase_order(li.purchase_order());
739             nodeByName('title_details', row).innerHTML = html;
740
741             nodeByName('title_details', row).parentNode.id = 'li-title-ref-' + li.id();
742             console.log(dojo.byId('li-title-ref-' + li.id()));
743
744             updateReceiveLink(li);
745
746             // set some default values if otherwise unset
747             if (!invoicePane.getFieldValue('receiver')) {
748                 invoicePane.setFieldValue('receiver', li.purchase_order().ordering_agency());
749             }
750             if (!invoicePane.getFieldValue('provider')) {
751                 invoicePane.setFieldValue('provider', li.purchase_order().provider());
752             }
753
754             dojo.forEach(
755                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
756                 function(field) {
757                     var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
758                     if(field.match(/count/)) {
759                         dijitArgs.style = 'width:4em;';
760                     } else {
761                         dijitArgs.style = 'width:9em;';
762                     }
763                     if(entry.isnew() && field == 'phys_item_count') {
764                         // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
765                         var count = Number(li.order_summary().item_count() || 0) - 
766                                     Number(li.order_summary().cancel_count() || 0) -
767                                     Number(li.order_summary().invoice_count() || 0);
768                         if(count < 0) count = 0;
769                         dijitArgs.value = count;
770                     }
771                     registerWidget(
772                         entry, 
773                         field,
774                         new openils.widget.AutoFieldWidget({
775                             fmObject : entry,
776                             fmClass : 'acqie',
777                             fmField : field,
778                             dijitArgs : dijitArgs,
779                             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
780                             parentNode : nodeByName(field, row)
781                         }),
782                         function(w) {    
783
784                             if(field == 'phys_item_count') {
785                                 dojo.connect(w, 'onChange', 
786                                     function() {
787                                         // staff entered a higher number in the receive field than was originally ordered
788                                         // taking into account already invoiced items
789                                         var extra = Number(this.attr('value')) - 
790                                             (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
791                                         if(extra > 0) {
792                                             storeExtraCopies(entry, extra);
793                                         }
794                                     }
795                                 )
796                             } // if
797
798                             if(field == 'inv_item_count' || field == 'cost_billed') {
799                                 setPerCopyPrice(row, entry);
800                                 // update the per-copy count as invoice count and cost billed change 
801                                 dojo.connect(w, 'onChange', function() { setPerCopyPrice(row, entry) } );
802                             } 
803
804                         } // func
805                     );
806                 }
807             );
808
809             updateTotalCost();
810             if (focusLineitem == li.id())
811                 focusLi();
812         }
813     );
814
815     nodeByName('detach', row).onclick = function() {
816         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
817         var idents = [];
818         dojo.forEach(['isbn', 'upc', 'issn'], 
819             function(ident) { 
820                 var val = liMarcAttr(entry.lineitem(), ident);
821                 if(val) idents.push(val); 
822             }
823         );
824
825         var msg = dojo.string.substitute(
826             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
827                 cost || 0,
828                 liMarcAttr(entry.lineitem(), 'title'),
829                 liMarcAttr(entry.lineitem(), 'author'),
830                 idents.join(',')
831             ]
832         );
833         if(!confirm(msg)) return;
834         entryTbody.removeChild(row);
835         entry.isdeleted(true);
836         if(entry.isnew())
837             delete widgetRegistry.acqie[entry.id()];
838         updateTotalCost();
839     }
840
841     entryTbody.appendChild(row);
842 }
843
844 function setPerCopyPrice(row, entry) {
845     var inv_w = widgetRegistry.acqie[entry.id()].inv_item_count;
846     var bill_w = widgetRegistry.acqie[entry.id()].cost_billed;
847
848     if (inv_w && bill_w) {
849         var invoiced = Number(inv_w.getFormattedValue());
850         var billed = Number(bill_w.getFormattedValue());
851         console.log(invoiced + ' : ' + billed);
852         if (invoiced > 0) {
853             nodeByName('amount_paid_per_copy', row).innerHTML = (billed / invoiced).toFixed(2);
854         } else {
855             nodeByName('amount_paid_per_copy', row).innerHTML = '0.00';
856         }
857     }
858 }
859
860 function liMarcAttr(lineitem, name) {
861     var attr = lineitem.attributes().filter(
862         function(attr) { 
863             if(
864                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
865                 attr.attr_name() == name) 
866                     return attr 
867         } 
868     )[0];
869     return (attr) ? attr.attr_value() : '';
870 }
871
872 function saveChanges(args) {
873     args = args || {};
874     createExtraCopies(function() { saveChangesPartTwo(args); });
875 }
876
877 // Define a helper function to 'unflesh' sub-objects from an fmclass object.
878 // 'this' specifies the object; the arguments specify a list of names of
879 // sub-objects.
880 function unflesh() {
881     var _, $ = this;
882     dojo.forEach(arguments, function (n) {
883         _ = $[n]();
884         if (_ !== null && typeof _ === 'object')
885             $[n]( _.id() );
886     });
887 }
888
889 function saveChangesPartTwo(args) {
890     args = args || {};
891
892     if(args.reopen) {
893         invoice.complete('f');
894
895     } else {
896
897         // Prepare an invoice for submission
898         if(!invoice) {
899             invoice = new fieldmapper.acqinv();
900             invoice.isnew(true);
901         } else {
902             invoice.ischanged(true); // for now, just always update
903         }
904
905         var e = invoicePane.mapValues(function (n, v) { invoice[n](v); });
906         if (e instanceof Error) {
907             alert(e.message);
908             return;
909         }
910
911         if(args.close)
912             invoice.complete('t');
913
914
915         // Prepare any charge items
916         var updateItems = [];
917         for(var id in widgetRegistry.acqii) {
918             var reg = widgetRegistry.acqii[id];
919             var item = reg._object;
920             if(item.ischanged() || item.isnew() || item.isdeleted()) {
921                 updateItems.push(item);
922                 if(item.isnew()) item.id(null);
923                 for(var field in reg) {
924                     if(field != '_object')
925                         item[field]( reg[field].getFormattedValue() );
926                 }
927                 
928                 unflesh.call(item, 'purchase_order');
929
930             }
931         }
932
933         // Prepare any line items
934         var updateEntries = [];
935         for(var id in widgetRegistry.acqie) {
936             var reg = widgetRegistry.acqie[id];
937             var entry = reg._object;
938             if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
939                 updateEntries.push(entry);
940                 if(entry.isnew()) entry.id(null);
941
942                 for(var field in reg) {
943                     if(field != '_object')
944                         entry[field]( reg[field].getFormattedValue() );
945                 }
946                 
947                 unflesh.call(entry, 'purchase_order', 'lineitem');
948             }
949         }
950     }
951
952     progressDialog.show(true);
953     fieldmapper.standardRequest(
954         ['open-ils.acq', 'open-ils.acq.invoice.update'],
955         {
956             params : [openils.User.authtoken, invoice, updateEntries, updateItems],
957             oncomplete : function(r) {
958                 progressDialog.hide();
959                 var invoice = openils.Util.readResponse(r);
960                 if(invoice) {
961                     if(args.prorate)
962                         return prorateInvoice(invoice);
963                     if (args.clear) {
964                         location.href = oilsBasePath + '/acq/invoice/view?create=1';
965                     } else {
966                         location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
967                     }
968                 }
969             }
970         }
971     );
972 }
973
974 function prorateInvoice(invoice) {
975     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
976     progressDialog.show(true);
977
978     fieldmapper.standardRequest(
979         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
980         {
981             params : [openils.User.authtoken, invoice.id()],
982             oncomplete : function(r) {
983                 progressDialog.hide();
984                 var invoice = openils.Util.readResponse(r);
985                 if(invoice) {
986                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
987                 }
988             }
989         }
990     );
991 }
992
993 function storeExtraCopies(entry, numExtra) {
994
995     dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
996         dojo.string.substitute(
997             localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
998
999     var addCopyHandler;
1000     addCopyHandler = dojo.connect(
1001         extraCopiesGo, 
1002         'onClick',
1003         function() {
1004             extraCopies[entry.lineitem().id()] = {
1005                 numExtra : numExtra, 
1006                 fund : extraCopiesFund.widget.attr('value')
1007             }
1008             extraItemsDialog.hide();
1009             dojo.disconnect(addCopyHandler);
1010         }
1011     );
1012
1013     dojo.connect(
1014         extraCopiesCancel, 
1015         'onClick',
1016         function() { 
1017             widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
1018             extraItemsDialog.hide() 
1019         }
1020     );
1021
1022     extraItemsDialog.show();
1023 }
1024
1025 function validateInvIdent(inv_ident, provider, receiver) {
1026     if (!(inv_ident && provider && receiver)) {
1027         console.info("not enough information to pre-validate inv_ident");
1028         return;
1029     }
1030
1031     openils.Util.show("ident-validation-spinner", "inline");
1032     var pcrud = new openils.PermaCrud();
1033     pcrud.search(
1034         "acqinv", {"inv_ident": inv_ident, "provider": provider}, {
1035             "oncomplete": function(r) {
1036                 openils.Util.hide("ident-validation-spinner");
1037
1038                 /* This could throw an event about the user not having perms,
1039                  * but in such a case the whole interface is already busted
1040                  * anyway. */
1041                 r = openils.Util.readResponse(r);
1042
1043                 var w = invoicePane.getFieldWidget("inv_ident").widget;
1044                 if (r.length) {
1045                     alert(localeStrings.INVOICE_IDENT_COLLIDE);
1046                     w.validator = function() { return false; };
1047                     w.validate();
1048                 } else {
1049                     w.validator = function() { return true; };
1050                     w.validate();
1051                 }
1052                 w.focus();
1053                 pcrud.disconnect();
1054             }
1055         }
1056     );
1057 }
1058
1059 function drawInvoicePane(parentNode, inv, args) {
1060     args = args || {};
1061     var pane;
1062
1063     var override = {};
1064     if(!inv) {
1065         override = {
1066             recv_date : {widgetValue : dojo.date.stamp.toISOString(new Date())},
1067             //receiver : {widgetValue : openils.User.user.ws_ou()},
1068             receiver : {
1069                 "dijitArgs": {
1070                     "onChange": function(val) {
1071                         validateInvIdent(
1072                             invoicePane && invoicePane.getFieldValue("inv_ident"),
1073                             invoicePane && invoicePane.getFieldValue("provider"),
1074                             val
1075                         );
1076                     }
1077                 }
1078             },
1079             recv_method : {widgetValue : 'PPR'}
1080         };
1081     }
1082
1083     dojo.mixin(override, {
1084         provider : { 
1085             dijitArgs : { 
1086                 store_options : { base_filter : { active :"t" } },
1087                 onChange : function(val) {
1088                     pane.setFieldValue('shipper', val);
1089                     validateInvIdent(
1090                         invoicePane && invoicePane.getFieldValue("inv_ident"),
1091                         val,
1092                         invoicePane && invoicePane.getFieldValue("receiver")
1093                     );
1094                 }
1095             } 
1096         },
1097         shipper  : { dijitArgs : { store_options : { base_filter : { active :"t" } } } }
1098     });
1099
1100     for(var field in args) {
1101         override[field] = {widgetValue : args[field]};
1102     }
1103
1104     // push the name of the invoice into the name display field after update
1105     override.inv_ident = dojo.mixin(
1106         override.inv_ident,
1107         {dijitArgs : {onChange :
1108             function(newVal) {
1109                 validateInvIdent(
1110                     newVal,
1111                     invoicePane && invoicePane.getFieldValue("provider"),
1112                     invoicePane && invoicePane.getFieldValue("receiver")
1113                 );
1114
1115                 if (dojo.byId('acq-invoice-summary-name'))
1116                     dojo.byId('acq-invoice-summary-name').innerHTML = newVal;
1117             }
1118         }}
1119     );
1120
1121
1122     pane = new openils.widget.EditPane({
1123         fmObject : inv,
1124         paneStackCount : 2,
1125         fmClass : 'acqinv',
1126         mode : (inv) ? 'edit' : 'create',
1127         hideActionButtons : true,
1128         overrideWidgetArgs : override,
1129         readOnly : (inv) && openils.Util.isTrue(inv.complete()),
1130         requiredFields : [
1131             'inv_ident', 
1132             'recv_date', 
1133             'provider', 
1134             'shipper'
1135         ],
1136         fieldOrder : [
1137             'inv_ident', 
1138             'recv_date', 
1139             'recv_method', 
1140             'inv_type', 
1141             'provider', 
1142             'shipper'
1143         ],
1144         suppressFields : ['id', 'complete']
1145     });
1146
1147     pane.startup();
1148     parentNode.appendChild(pane.domNode);
1149     return pane;
1150 }
1151
1152
1153 function createExtraCopies(oncomplete) {
1154
1155     var lids = [];
1156     for(var liId in extraCopies) {
1157         var data = extraCopies[liId];
1158         for(var i = 0; i < data.numExtra; i++) {
1159             var lid = new fieldmapper.acqlid();
1160             lid.isnew(true);
1161             lid.lineitem(liId);
1162             lid.fund(data.fund);
1163             lid.recv_time('now');
1164             lids.push(lid);
1165         }
1166     }
1167
1168     if(lids.length == 0) 
1169         return oncomplete();
1170
1171     fieldmapper.standardRequest(
1172         ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
1173         {
1174             params : [openils.User.authtoken, lids, true],
1175             oncomplete : function(r) {
1176                 if(openils.Util.readResponse(r))
1177                     oncomplete();
1178             }
1179         }
1180     );
1181
1182 }
1183
1184
1185 openils.Util.addOnLoad(init);
1186
1187