]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
42f83771a7c5186251956ea0b591bc6fdf530ea0
[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             )
594         }
595     );
596
597
598     /* ----------- fund -------------- */
599     var fundArgs = {
600         fmClass : 'acqii',
601         fmObject : item,
602         fmField : 'fund',
603         labelFormat : fundLabelFormat,
604         searchFormat : fundSearchFormat,
605         readOnly : invoice && openils.Util.isTrue(invoice.complete()),
606         dijitArgs : {required : true},
607         parentNode : nodeByName('fund', row)
608     }
609
610     if(item.fund_debit()) {
611         fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
612     } else {
613         fundArgs.searchFilter = {active : 't'}
614         if(itemType && openils.Util.isTrue(itemType.prorate()))
615             fundArgs.dijitArgs = {disabled : true};
616     }
617
618     var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
619     registerWidget(item, 'fund', fundWidget);
620
621     /* ---------- inv_item_type ------------- */
622
623     if(item.po_item()) {
624
625         // read-only item view for items that were the result of a po-item
626         var po = item.purchase_order();
627         var po_item = item.po_item();
628         var node = nodeByName('inv_item_type', row);
629         var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
630         orderDate = (!po.order_date()) ? '' : 
631                 dojo.date.locale.format(dojo.date.stamp.fromISOString(po.order_date()), {selector:'date'});
632
633         node.innerHTML = dojo.string.substitute(
634             localeStrings.INVOICE_ITEM_PO_DETAILS, 
635             [ 
636                 itemType.name(),
637                 oilsBasePath, 
638                 po.id(), 
639                 po.name(), 
640                 orderDate,
641                 po_item.estimated_cost() 
642             ]
643         );
644
645     } else {
646
647         registerWidget(
648             item,
649             'inv_item_type',
650             new openils.widget.AutoFieldWidget({
651                 fmObject : item,
652                 fmField : 'inv_item_type',
653                 parentNode : nodeByName('inv_item_type', row),
654                 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
655                 dijitArgs : {required : true}
656             }),
657             function(w, ww) {
658                 // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
659                 // since this charge will be prorated against (potentially) multiple funds
660                 dojo.connect(w, 'onChange', 
661                     function() {
662                         if(!item.fund_debit()) {
663                             var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
664                             if(!itemType) return;
665                             if(openils.Util.isTrue(itemType.prorate())) {
666                                 fundWidget.widget.attr('disabled', true);
667                                 fundWidget.widget.attr('value', '');
668                             } else {
669                                 fundWidget.widget.attr('disabled', false);
670                             }
671                         }
672                     }
673                 );
674             }
675         );
676     }
677
678     nodeByName('delete', row).onclick = function() {
679         var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
680         var msg = dojo.string.substitute(
681             localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
682                 cost || 0,
683                 widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue() || ''
684             ]
685         );
686         if(!confirm(msg)) return;
687         itemTbody.removeChild(row);
688         item.isdeleted(true);
689         if(item.isnew())
690             delete widgetRegistry.acqii[item.id()];
691         updateTotalCost();
692     }
693
694     itemTbody.appendChild(row);
695     updateTotalCost();
696 }
697
698 function updateReceiveLink(li) {
699     if (!invoiceId)
700         return; /* can't do this with unsaved invoices */
701
702     var link = dojo.byId("acq-view-invoice-receive-link");
703     if (link.onclick) return; /* only need to do this once */
704
705     /* don't do this if there's nothing receivable on the lineitem */
706     if (li.order_summary().recv_count() + li.order_summary().cancel_count() >=
707         li.order_summary().item_count())
708         return;
709
710     openils.Util.show("acq-view-invoice-receive");
711     link.onclick = function() { location.href =  oilsBasePath + '/acq/invoice/receive/' + invoiceId; };
712 }
713
714 /*
715  * Ensures focusLineitem is in view and causes a brief 
716  * border around the lineitem to come to life then fade.
717  */
718 function focusLi() {
719     if (!focusLineitem) return;
720
721     // set during addLineitem()
722     var node = dojo.byId('li-title-ref-' + focusLineitem);
723
724     console.log('focus: li-title-ref-' + focusLineitem + ' : ' + node);
725
726     // LI may not yet be rendered
727     if (!node) return; 
728
729     console.log('focusing ' + focusLineitem);
730
731     // prevent numerous re-focuses
732     focusLineitem = null; 
733
734     // causes the full row to be visible
735     dijit.scrollIntoView(node);
736
737     dojo.require('dojox.fx');
738
739     setTimeout(
740         function() {
741             dojox.fx.highlight({color : '#BB4433', node : node, duration : 2000}).play();
742         }, 
743     100);
744 }
745
746
747 // expected cost is totalCostInvoiced + totalCostNotYetInvoiced
748 function updateExpectedCost() {
749
750     var cost = Number(totalInvoicedBox.innerHTML || 0);
751
752     // for any LI's that are not yet billed (i.e. filled in)
753     // use the total expected cost for that lineitem.
754     for(var id in widgetRegistry.acqie) {
755         var entry = widgetRegistry.acqie[id]._object;
756         if(!entry.isdeleted()) {
757             if (Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue()) == 0) {
758                 var li = entry.lineitem();
759                 cost += 
760                     Number(li.order_summary().estimated_amount()) - 
761                     Number(li.order_summary().paid_amount());
762             }
763         }
764     }
765
766     dojo.byId('acq-invoice-summary-cost').innerHTML = cost.toFixed(2);
767 }
768
769 var invoicEntryWidgets = {};
770 function addInvoiceEntry(entry) {
771     console.log('Adding new entry for lineitem ' + entry.lineitem());
772
773     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
774     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
775     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
776
777     dojo.byId('acq-invoice-summary-count').innerHTML = 
778         Number(dojo.byId('acq-invoice-summary-count').innerHTML) + 1;
779
780     entryTbody = dojo.byId('acq-invoice-entry-tbody');
781     if(entryTemplate == null) {
782         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
783     }
784
785     if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
786         // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
787         return;
788
789     var row = entryTemplate.cloneNode(true);
790     row.setAttribute('lineitem', entry.lineitem());
791     row.setAttribute('entry_lineitem_row', entry.lineitem());
792
793     openils.acq.Lineitem.fetchAndRender(
794         entry.lineitem(), {}, 
795         function(li, html) { 
796             entry.lineitem(li);
797             entry.purchase_order(li.purchase_order());
798             nodeByName('title_details', row).innerHTML = html;
799
800             nodeByName('title_details', row).parentNode.id = 'li-title-ref-' + li.id();
801             console.log(dojo.byId('li-title-ref-' + li.id()));
802
803             updateReceiveLink(li);
804
805             // set some default values if otherwise unset
806             if (!invoicePane.getFieldValue('receiver')) {
807                 invoicePane.setFieldValue('receiver', li.purchase_order().ordering_agency());
808             }
809             if (!invoicePane.getFieldValue('provider')) {
810                 invoicePane.setFieldValue('provider', li.purchase_order().provider());
811             }
812
813             dojo.forEach(
814                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
815                 function(field) {
816                     var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
817                     if(field.match(/count/)) {
818                         dijitArgs.style = 'width:4em;';
819                     } else {
820                         dijitArgs.style = 'width:9em;';
821                     }
822                     if(entry.isnew() && field == 'phys_item_count') {
823                         // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
824                         var count = Number(li.order_summary().item_count() || 0) - 
825                                     Number(li.order_summary().cancel_count() || 0) -
826                                     Number(li.order_summary().invoice_count() || 0);
827                         if(count < 0) count = 0;
828                         dijitArgs.value = count;
829                     }
830                     registerWidget(
831                         entry, 
832                         field,
833                         new openils.widget.AutoFieldWidget({
834                             fmObject : entry,
835                             fmClass : 'acqie',
836                             fmField : field,
837                             dijitArgs : dijitArgs,
838                             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
839                             parentNode : nodeByName(field, row)
840                         }),
841                         function(w) {    
842
843                             if(field == 'phys_item_count') {
844                                 dojo.connect(w, 'onChange', 
845                                     function() {
846                                         // staff entered a higher number in the receive field than was originally ordered
847                                         // taking into account already invoiced items
848                                         var extra = Number(this.attr('value')) - 
849                                             (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
850                                         if(extra > 0) {
851                                             storeExtraCopies(entry, extra);
852                                         }
853                                     }
854                                 )
855                             } // if
856
857                             if(field == 'inv_item_count' || field == 'cost_billed') {
858                                 setPerCopyPrice(row, entry);
859                                 // update the per-copy count as invoice count and cost billed change 
860                                 dojo.connect(w, 'onChange', function() { setPerCopyPrice(row, entry) } );
861                             } 
862
863                         } // func
864                     );
865                 }
866             );
867
868             updateTotalCost();
869             if (focusLineitem == li.id())
870                 focusLi();
871         }
872     );
873
874     nodeByName('detach', row).onclick = function() {
875         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
876         var idents = [];
877         dojo.forEach(['isbn', 'upc', 'issn'], 
878             function(ident) { 
879                 var val = liMarcAttr(entry.lineitem(), ident);
880                 if(val) idents.push(val); 
881             }
882         );
883
884         var msg = dojo.string.substitute(
885             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
886                 cost || 0,
887                 liMarcAttr(entry.lineitem(), 'title'),
888                 liMarcAttr(entry.lineitem(), 'author'),
889                 idents.join(',')
890             ]
891         );
892         if(!confirm(msg)) return;
893         entryTbody.removeChild(row);
894         entry.isdeleted(true);
895         if(entry.isnew())
896             delete widgetRegistry.acqie[entry.id()];
897         updateTotalCost();
898     }
899
900     entryTbody.appendChild(row);
901 }
902
903 function setPerCopyPrice(row, entry) {
904     var inv_w = widgetRegistry.acqie[entry.id()].inv_item_count;
905     var bill_w = widgetRegistry.acqie[entry.id()].cost_billed;
906
907     if (inv_w && bill_w) {
908         var invoiced = Number(inv_w.getFormattedValue());
909         var billed = Number(bill_w.getFormattedValue());
910         console.log(invoiced + ' : ' + billed);
911         if (invoiced > 0) {
912             nodeByName('amount_paid_per_copy', row).innerHTML = (billed / invoiced).toFixed(2);
913         } else {
914             nodeByName('amount_paid_per_copy', row).innerHTML = '0.00';
915         }
916     }
917 }
918
919 function liMarcAttr(lineitem, name) {
920     var attr = lineitem.attributes().filter(
921         function(attr) { 
922             if(
923                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
924                 attr.attr_name() == name) 
925                     return attr 
926         } 
927     )[0];
928     return (attr) ? attr.attr_value() : '';
929 }
930
931 function saveChanges(args) {
932     args = args || {};
933     createExtraCopies(function() { saveChangesPartTwo(args); });
934 }
935
936 // Define a helper function to 'unflesh' sub-objects from an fmclass object.
937 // 'this' specifies the object; the arguments specify a list of names of
938 // sub-objects.
939 function unflesh() {
940     var _, $ = this;
941     dojo.forEach(arguments, function (n) {
942         _ = $[n]();
943         if (_ !== null && typeof _ === 'object')
944             $[n]( _.id() );
945     });
946 }
947
948 function saveChangesPartTwo(args) {
949     args = args || {};
950
951     if(args.reopen) {
952         invoice.complete('f');
953
954     } else {
955
956         // Prepare an invoice for submission
957         if(!invoice) {
958             invoice = new fieldmapper.acqinv();
959             invoice.isnew(true);
960         } else {
961             invoice.ischanged(true); // for now, just always update
962         }
963
964         var e = invoicePane.mapValues(function (n, v) { invoice[n](v); });
965         if (e instanceof Error) {
966             alert(e.message);
967             return;
968         }
969
970         if(args.close)
971             invoice.complete('t');
972
973
974         // Prepare any charge items
975         var updateItems = [];
976         for(var id in widgetRegistry.acqii) {
977             var reg = widgetRegistry.acqii[id];
978             var item = reg._object;
979             if(item.ischanged() || item.isnew() || item.isdeleted()) {
980                 updateItems.push(item);
981                 if(item.isnew()) item.id(null);
982                 for(var field in reg) {
983                     if(field != '_object')
984                         item[field]( reg[field].getFormattedValue() );
985                 }
986                 
987                 unflesh.call(item, 'purchase_order');
988
989             }
990         }
991
992         // Prepare any line items
993         var updateEntries = [];
994         for(var id in widgetRegistry.acqie) {
995             var reg = widgetRegistry.acqie[id];
996             var entry = reg._object;
997             if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
998                 updateEntries.push(entry);
999                 if(entry.isnew()) entry.id(null);
1000
1001                 for(var field in reg) {
1002                     if(field != '_object')
1003                         entry[field]( reg[field].getFormattedValue() );
1004                 }
1005                 
1006                 unflesh.call(entry, 'purchase_order', 'lineitem');
1007             }
1008         }
1009     }
1010
1011     progressDialog.show(true);
1012     fieldmapper.standardRequest(
1013         ['open-ils.acq', 'open-ils.acq.invoice.update'],
1014         {
1015             params : [openils.User.authtoken, invoice, updateEntries, updateItems],
1016             oncomplete : function(r) {
1017                 progressDialog.hide();
1018                 var invoice = openils.Util.readResponse(r);
1019                 if(invoice) {
1020                     if(args.prorate)
1021                         return prorateInvoice(invoice);
1022                     if (args.clear) {
1023                         location.href = oilsBasePath + '/acq/invoice/view?create=1';
1024                     } else {
1025                         location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
1026                     }
1027                 }
1028             }
1029         }
1030     );
1031 }
1032
1033 function prorateInvoice(invoice) {
1034     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
1035     progressDialog.show(true);
1036
1037     fieldmapper.standardRequest(
1038         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
1039         {
1040             params : [openils.User.authtoken, invoice.id()],
1041             oncomplete : function(r) {
1042                 progressDialog.hide();
1043                 var invoice = openils.Util.readResponse(r);
1044                 if(invoice) {
1045                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
1046                 }
1047             }
1048         }
1049     );
1050 }
1051
1052 function storeExtraCopies(entry, numExtra) {
1053
1054     dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
1055         dojo.string.substitute(
1056             localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
1057
1058     var addCopyHandler;
1059     addCopyHandler = dojo.connect(
1060         extraCopiesGo, 
1061         'onClick',
1062         function() {
1063             extraCopies[entry.lineitem().id()] = {
1064                 numExtra : numExtra, 
1065                 fund : extraCopiesFund.widget.attr('value')
1066             }
1067             extraItemsDialog.hide();
1068             dojo.disconnect(addCopyHandler);
1069         }
1070     );
1071
1072     dojo.connect(
1073         extraCopiesCancel, 
1074         'onClick',
1075         function() { 
1076             widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
1077             extraItemsDialog.hide() 
1078         }
1079     );
1080
1081     extraItemsDialog.show();
1082 }
1083
1084 function validateInvIdent(inv_ident, provider, receiver) {
1085     if (!(inv_ident && provider && receiver)) {
1086         console.info("not enough information to pre-validate inv_ident");
1087         return;
1088     }
1089
1090     openils.Util.show("ident-validation-spinner", "inline");
1091     var pcrud = new openils.PermaCrud();
1092     pcrud.search(
1093         "acqinv", {"inv_ident": inv_ident, "provider": provider}, {
1094             "oncomplete": function(r) {
1095                 openils.Util.hide("ident-validation-spinner");
1096
1097                 /* This could throw an event about the user not having perms,
1098                  * but in such a case the whole interface is already busted
1099                  * anyway. */
1100                 r = openils.Util.readResponse(r);
1101
1102                 var w = invoicePane.getFieldWidget("inv_ident").widget;
1103                 if (r.length) {
1104                     alert(localeStrings.INVOICE_IDENT_COLLIDE);
1105                     w.validator = function() { return false; };
1106                     w.validate();
1107                 } else {
1108                     w.validator = function() { return true; };
1109                     w.validate();
1110                 }
1111                 w.focus();
1112                 pcrud.disconnect();
1113             }
1114         }
1115     );
1116 }
1117
1118 function drawInvoicePane(parentNode, inv, args) {
1119     args = args || {};
1120     var pane;
1121
1122     var override = {};
1123     if(!inv) {
1124         override = {
1125             recv_date : {widgetValue : dojo.date.stamp.toISOString(new Date())},
1126             //receiver : {widgetValue : openils.User.user.ws_ou()},
1127             receiver : {
1128                 "dijitArgs": {
1129                     "onChange": function(val) {
1130                         validateInvIdent(
1131                             invoicePane && invoicePane.getFieldValue("inv_ident"),
1132                             invoicePane && invoicePane.getFieldValue("provider"),
1133                             val
1134                         );
1135                     }
1136                 }
1137             },
1138             recv_method : {widgetValue : 'PPR'}
1139         };
1140     }
1141
1142     dojo.mixin(override, {
1143         provider : { 
1144             dijitArgs : { 
1145                 store_options : { base_filter : { active :"t" } },
1146                 onChange : function(val) {
1147                     pane.setFieldValue('shipper', val);
1148                     validateInvIdent(
1149                         invoicePane && invoicePane.getFieldValue("inv_ident"),
1150                         val,
1151                         invoicePane && invoicePane.getFieldValue("receiver")
1152                     );
1153                 }
1154             } 
1155         },
1156         shipper  : { dijitArgs : { store_options : { base_filter : { active :"t" } } } }
1157     });
1158
1159     for(var field in args) {
1160         override[field] = {widgetValue : args[field]};
1161     }
1162
1163     // push the name of the invoice into the name display field after update
1164     override.inv_ident = dojo.mixin(
1165         override.inv_ident,
1166         {dijitArgs : {onChange :
1167             function(newVal) {
1168                 validateInvIdent(
1169                     newVal,
1170                     invoicePane && invoicePane.getFieldValue("provider"),
1171                     invoicePane && invoicePane.getFieldValue("receiver")
1172                 );
1173
1174                 if (dojo.byId('acq-invoice-summary-name'))
1175                     dojo.byId('acq-invoice-summary-name').innerHTML = newVal;
1176             }
1177         }}
1178     );
1179
1180
1181     pane = new openils.widget.EditPane({
1182         fmObject : inv,
1183         paneStackCount : 2,
1184         fmClass : 'acqinv',
1185         mode : (inv) ? 'edit' : 'create',
1186         hideActionButtons : true,
1187         overrideWidgetArgs : override,
1188         readOnly : (inv) && openils.Util.isTrue(inv.complete()),
1189         requiredFields : [
1190             'inv_ident', 
1191             'recv_date', 
1192             'provider', 
1193             'shipper'
1194         ],
1195         fieldOrder : [
1196             'inv_ident', 
1197             'recv_date', 
1198             'recv_method', 
1199             'inv_type', 
1200             'provider', 
1201             'shipper'
1202         ],
1203         suppressFields : ['id', 'complete']
1204     });
1205
1206     pane.startup();
1207     parentNode.appendChild(pane.domNode);
1208     return pane;
1209 }
1210
1211
1212 function createExtraCopies(oncomplete) {
1213
1214     var lids = [];
1215     for(var liId in extraCopies) {
1216         var data = extraCopies[liId];
1217         for(var i = 0; i < data.numExtra; i++) {
1218             var lid = new fieldmapper.acqlid();
1219             lid.isnew(true);
1220             lid.lineitem(liId);
1221             lid.fund(data.fund);
1222             lid.recv_time('now');
1223             lids.push(lid);
1224         }
1225     }
1226
1227     if(lids.length == 0) 
1228         return oncomplete();
1229
1230     fieldmapper.standardRequest(
1231         ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
1232         {
1233             params : [openils.User.authtoken, lids, true],
1234             oncomplete : function(r) {
1235                 if(openils.Util.readResponse(r))
1236                     oncomplete();
1237             }
1238         }
1239     );
1240
1241 }
1242
1243 function smartSearchSubmitter() {
1244     performSearch(0, !dojo.byId('acq-unified-build-progressively').checked);
1245 }
1246
1247
1248 openils.Util.addOnLoad(init);
1249
1250