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