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