]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
added a balance owed column to invoice UI
[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('dijit.form.CheckBox');
4 dojo.require('dijit.form.CurrencyTextBox');
5 dojo.require('dijit.form.NumberTextBox');
6 dojo.require('openils.User');
7 dojo.require('openils.Util');
8 dojo.require('openils.CGI');
9 dojo.require('openils.PermaCrud');
10 dojo.require('openils.widget.EditPane');
11 dojo.require('openils.widget.AutoFieldWidget');
12 dojo.require('openils.widget.ProgressDialog');
13
14 dojo.requireLocalization('openils.acq', 'acq');
15 var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
16
17 var fundLabelFormat = ['${0} (${1})', 'code', 'year'];
18 var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
19
20 var cgi = new openils.CGI();
21 var pcrud = new openils.PermaCrud();
22 var attachLi;
23 var attachPo;
24 var invoice;
25 var itemTbody;
26 var itemTemplate;
27 var entryTemplate;
28 var totalInvoicedBox;
29 var totalPaidBox;
30 var balanceOwedBox;
31 var invoicePane;
32 var itemTypes;
33 var virtualId = -1;
34 var widgetRegistry = {acqie : {}, acqii : {}};
35
36 function nodeByName(name, context) {
37     return dojo.query('[name='+name+']', context)[0];
38 }
39
40 function init() {
41
42     attachLi = cgi.param('attach_li');
43     attachPo = cgi.param('attach_po');
44
45     itemTypes = pcrud.retrieveAll('aiit');
46
47     if(cgi.param('create')) {
48         renderInvoice();
49
50     } else {
51         fieldmapper.standardRequest(
52             ['open-ils.acq', 'open-ils.acq.invoice.retrieve'],
53             {
54                 params : [openils.User.authtoken, invoiceId],
55                 oncomplete : function(r) {
56                     invoice = openils.Util.readResponse(r);     
57                     renderInvoice();
58                 }
59             }
60         );
61     }
62 }
63
64 function renderInvoice() {
65
66     // in create mode, let the LI or PO render the invoice with seed data
67     if( !(cgi.param('create') && (attachPo || attachLi)) ) {
68         invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), invoice);
69     }
70
71     dojo.byId('acq-invoice-new-item').onclick = function() {
72         var item = new fieldmapper.acqii();
73         item.id(virtualId--);
74         item.isnew(true);
75         addInvoiceItem(item);
76     }
77
78     updateTotalCost();
79
80     if(invoice) {
81         dojo.forEach(
82             invoice.items(),
83             function(item) {
84                 addInvoiceItem(item);
85             }
86         );
87
88         dojo.forEach(
89             invoice.entries(),
90             function(entry) {
91                 addInvoiceEntry(entry);
92             }
93         );
94     }
95
96     if(attachLi) doAttachLi();
97     if(attachPo) doAttachPo();
98 }
99
100 function doAttachLi() {
101
102     fieldmapper.standardRequest(
103         ["open-ils.acq", "open-ils.acq.lineitem.retrieve"], {
104             async: true,
105             params: [openils.User.authtoken, attachLi, {
106                 clear_marc : true,
107                 flesh_attrs : true,
108                 flesh_po : true,
109                 flesh_li_details : true,
110                 flesh_fund_debit : true
111             }],
112             oncomplete: function(r) { 
113                 lineitem = openils.Util.readResponse(r);
114
115                 if(cgi.param('create')) {
116                     // render the invoice using some seed data from the Lineitem
117                     var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; 
118                     invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
119                 }
120
121                 var entry = new fieldmapper.acqie();
122                 entry.id(virtualId--);
123                 entry.isnew(true);
124                 entry.lineitem(lineitem);
125                 entry.purchase_order(lineitem.purchase_order());
126                 addInvoiceEntry(entry);
127             }
128         }
129     );
130 }
131
132 function doAttachPo() {
133     fieldmapper.standardRequest(
134         ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
135         {   async: true,
136             params: [openils.User.authtoken, attachPo, {
137                 flesh_lineitems : true,
138                 clear_marc : true,
139                 flesh_lineitem_details : true,
140                 flesh_fund_debit : true
141             }],
142             oncomplete: function(r) {
143                 var po = openils.Util.readResponse(r);
144
145                 if(cgi.param('create')) {
146                     // render the invoice using some seed data from the PO
147                     var invoiceArgs = {provider : po.provider(), shipper : po.provider()}; 
148                     invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
149                 }
150
151                 dojo.forEach(po.lineitems(), 
152                     function(lineitem) {
153                         var entry = new fieldmapper.acqie();
154                         entry.id(virtualId--);
155                         entry.isnew(true);
156                         entry.lineitem(lineitem);
157                         entry.purchase_order(po);
158                         lineitem.purchase_order(po);
159                         addInvoiceEntry(entry);
160                     }
161                 );
162             }
163         }
164     );
165 }
166
167 function updateTotalCost() {
168
169     var totalCost = 0;    
170     if(!totalInvoicedBox) {
171         totalInvoicedBox = new dijit.form.CurrencyTextBox(
172             {style : 'width: 6em'}, dojo.byId('acq-invoice-total-invoiced'));
173     }
174     for(var id in widgetRegistry.acqii) 
175         if(!widgetRegistry.acqii[id]._object.isdeleted())
176             totalCost += widgetRegistry.acqii[id].cost_billed.getFormattedValue();
177     for(var id in widgetRegistry.acqie) 
178         if(!widgetRegistry.acqie[id]._object.isdeleted())
179             totalCost += widgetRegistry.acqie[id].cost_billed.getFormattedValue();
180     totalInvoicedBox.attr('value', totalCost);
181
182     totalPaid = 0;    
183     if(!totalPaidBox) {
184         totalPaidBox = new dijit.form.CurrencyTextBox(
185             {style : 'width: 6em'}, dojo.byId('acq-invoice-total-paid'));
186     }
187     for(var id in widgetRegistry.acqii) 
188         if(!widgetRegistry.acqii[id]._object.isdeleted())
189             totalPaid += widgetRegistry.acqii[id].amount_paid.getFormattedValue();
190     for(var id in widgetRegistry.acqie) 
191         if(!widgetRegistry.acqie[id]._object.isdeleted())
192             totalPaid += widgetRegistry.acqie[id].amount_paid.getFormattedValue();
193     totalPaidBox.attr('value', totalPaid);
194
195     var buttonsDisabled = false;
196     if(totalPaid > totalCost || totalPaid < 0) {
197         openils.Util.addCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
198         invoiceSaveButton.attr('disabled', true);
199         invoiceProrateButton.attr('disabled', true);
200         buttonsDisabled = true;
201     } else {
202         openils.Util.removeCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
203         invoiceSaveButton.attr('disabled', false);
204         invoiceProrateButton.attr('disabled', false);
205     }
206
207     if(totalCost < 0) {
208         openils.Util.addCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
209         invoiceSaveButton.attr('disabled', true);
210         invoiceProrateButton.attr('disabled', true);
211     } else {
212         openils.Util.removeCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
213         if(!buttonsDisabled) {
214             invoiceSaveButton.attr('disabled', false);
215             invoiceProrateButton.attr('disabled', false);
216         }
217     }
218
219     if(!balanceOwedBox) {
220         balanceOwedBox = new dijit.form.CurrencyTextBox(
221             {style : 'width: 6em'}, dojo.byId('acq-invoice-balance-owed'));
222     }
223     balanceOwedBox.attr('value', (totalCost - totalPaid));
224 }
225
226
227 function registerWidget(obj, field, widget, callback) {
228     var blob = widgetRegistry[obj.classname];
229     if(!blob[obj.id()]) 
230         blob[obj.id()] = {_object : obj};
231     blob[obj.id()][field] = widget;
232     widget.build(
233         function(w, ww) {
234             dojo.connect(w, 'onChange', 
235                 function(newVal) { 
236                     obj.ischanged(true); 
237                     updateTotalCost();
238                 }
239             );
240             if(callback) callback(w, ww);
241         }
242     );
243     return widget;
244 }
245
246 function addInvoiceItem(item) {
247     itemTbody = dojo.byId('acq-invoice-item-tbody');
248     if(itemTemplate == null) {
249         itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
250     }
251
252     var row = itemTemplate.cloneNode(true);
253     var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
254
255     dojo.forEach(
256         ['title', 'author', 'cost_billed', 'amount_paid'], 
257         function(field) {
258             registerWidget(
259                 item,
260                 field,
261                 new openils.widget.AutoFieldWidget({
262                     fmClass : 'acqii',
263                     fmObject : item,
264                     fmField : field,
265                     dijitArgs : (field == 'cost_billed' || field == 'amount_paid') ? {required : true, style : 'width: 6em'} : null,
266                     parentNode : nodeByName(field, row)
267                 })
268             )
269         }
270     );
271
272
273     /* ----------- fund -------------- */
274     var fundArgs = {
275         fmClass : 'acqii',
276         fmObject : item,
277         fmField : 'fund',
278         labelFormat : fundLabelFormat,
279         searchFormat : fundSearchFormat,
280         parentNode : nodeByName('fund', row)
281     }
282
283     if(item.fund_debit()) {
284         fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
285     } else {
286         fundArgs.searchFilter = {active : 't'}
287         if(itemType && openils.Util.isTrue(itemType.prorate()))
288             fundArgs.dijitArgs = {disabled : true};
289     }
290
291     var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
292     registerWidget(item, 'fund', fundWidget);
293
294     /* ---------- inv_item_type ------------- */
295
296     registerWidget(
297         item,
298         'inv_item_type',
299         new openils.widget.AutoFieldWidget({
300             fmObject : item,
301             fmField : 'inv_item_type',
302             parentNode : nodeByName('inv_item_type', row),
303             dijitArgs : {required : true}
304         }),
305         function(w, ww) {
306             // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
307             // since this charge will be prorated against (potentially) multiple funds
308             dojo.connect(w, 'onChange', 
309                 function() {
310                     if(!item.fund_debit()) {
311                         var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
312                         if(!itemType) return;
313                         if(openils.Util.isTrue(itemType.prorate())) {
314                             fundWidget.widget.attr('disabled', true);
315                             fundWidget.widget.attr('value', '');
316                         } else {
317                             fundWidget.widget.attr('disabled', false);
318                         }
319                     }
320                 }
321             );
322         }
323     );
324
325     nodeByName('delete', row).onclick = function() {
326         var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
327         var msg = dojo.string.substitute(
328             localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
329                 cost,
330                 widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue()
331             ]
332         );
333         if(!confirm(msg)) return;
334         itemTbody.removeChild(row);
335         item.isdeleted(true);
336         if(item.isnew())
337             delete widgetRegistry.acqii[item.id()];
338         updateTotalCost();
339     }
340
341     itemTbody.appendChild(row);
342     updateTotalCost();
343 }
344
345 function addInvoiceEntry(entry) {
346     entryTbody = dojo.byId('acq-invoice-entry-tbody');
347     if(entryTemplate == null) {
348         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
349     }
350
351     if(dojo.query('[lineitem=' + entry.lineitem().id() +']', entryTbody)[0])
352         // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
353         return;
354
355     var row = entryTemplate.cloneNode(true);
356     row.setAttribute('lineitem', entry.lineitem().id());
357     var lineitem = entry.lineitem();
358
359     var idents = [];
360     if(liMarcAttr(lineitem, 'isbn')) idents.push(liMarcAttr(lineitem, 'isbn'));
361     if(liMarcAttr(lineitem, 'upc')) idents.push(liMarcAttr(lineitem, 'upc'));
362     if(liMarcAttr(lineitem, 'issn')) idents.push(liMarcAttr(lineitem, 'issn'));
363
364     var lids = lineitem.lineitem_details();
365     var numOrdered = lids.length;
366     var numReceived = lids.filter(function(lid) { return (lid.recv_time() != null) }).length;
367     var numInvoiced = lids.filter(function(lid) { return !openils.Util.isTrue(lid.fund_debit().encumbrance()) }).length;
368
369     var poName = '';
370     var poId = '';
371     var po = entry.purchase_order();
372     if(po) {
373         poName = po.name();
374         poId = po.id();
375     }
376
377     nodeByName('title_details', row).innerHTML = 
378         dojo.string.substitute(
379             localeStrings.INVOICE_TITLE_DETAILS, [
380                 liMarcAttr(lineitem, 'title'),
381                 liMarcAttr(lineitem, 'author'),
382                 idents.join(','),
383                 numOrdered,
384                 numReceived,
385                 Number(lineitem.estimated_unit_price()).toFixed(2),
386                 (Number(lineitem.estimated_unit_price()) * numOrdered).toFixed(2),
387                 numInvoiced,
388                 lineitem.id(),
389                 oilsBasePath,
390                 poId,
391                 poName
392             ]
393         );
394
395
396     dojo.forEach(
397         ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
398         function(field) {
399             var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
400             if(entry.isnew() && field == 'phys_item_count') dijitArgs.value = numReceived;
401             registerWidget(
402                 entry, 
403                 field,
404                 new openils.widget.AutoFieldWidget({
405                     fmObject : entry,
406                     fmClass : 'acqie',
407                     fmField : field,
408                     dijitArgs : dijitArgs,
409                     parentNode : nodeByName(field, row)
410                 })
411             );
412         }
413     );
414
415     nodeByName('detach', row).onclick = function() {
416         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
417         var msg = dojo.string.substitute(
418             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
419                 cost || 0,
420                 liMarcAttr(lineitem, 'title'),
421                 liMarcAttr(lineitem, 'author'),
422                 idents.join(',')
423             ]
424         );
425         if(!confirm(msg)) return;
426         entryTbody.removeChild(row);
427         entry.isdeleted(true);
428         if(entry.isnew())
429             delete widgetRegistry.acqie[entry.id()];
430         updateTotalCost();
431     }
432
433     entryTbody.appendChild(row);
434     updateTotalCost();
435 }
436
437 function liMarcAttr(lineitem, name) {
438     var attr = lineitem.attributes().filter(
439         function(attr) { 
440             if(
441                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
442                 attr.attr_name() == name) 
443                     return attr 
444         } 
445     )[0];
446     return (attr) ? attr.attr_value() : '';
447 }
448
449 function saveChanges(doProrate) {
450     
451     progressDialog.show(true);
452
453     var updateItems = [];
454     for(var id in widgetRegistry.acqii) {
455         var reg = widgetRegistry.acqii[id];
456         var item = reg._object;
457         if(item.ischanged() || item.isnew() || item.isdeleted()) {
458             updateItems.push(item);
459             if(item.isnew()) item.id(null);
460             for(var field in reg) {
461                 if(field != '_object')
462                     item[field]( reg[field].getFormattedValue() );
463             }
464             
465             // unflesh
466             if(item.purchase_order() != null && typeof item.purchase_order() == 'object')
467                 item.purchase_order( item.purchase_order().id() );
468         }
469     }
470
471     var updateEntries = [];
472     for(var id in widgetRegistry.acqie) {
473         var reg = widgetRegistry.acqie[id];
474         var entry = reg._object;
475         if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
476             entry.lineitem(entry.lineitem().id());
477             entry.purchase_order(entry.purchase_order().id());
478             updateEntries.push(entry);
479             if(entry.isnew()) entry.id(null);
480
481             for(var field in reg) {
482                 if(field != '_object')
483                     entry[field]( reg[field].getFormattedValue() );
484             }
485             
486             // unflesh
487             dojo.forEach(['purchase_order', 'lineitem'],
488                 function(field) {
489                     if(entry[field]() != null && typeof entry[field]() == 'object')
490                         entry[field]( entry[field]().id() );
491                 }
492             );
493         }
494     }
495
496     if(!invoice) {
497         invoice = new fieldmapper.acqinv();
498         invoice.isnew(true);
499     } else {
500         invoice.ischanged(true); // for now, just always update
501     }
502
503     dojo.forEach(invoicePane.fieldList, 
504         function(field) {
505             invoice[field.name]( field.widget.getFormattedValue() );
506         }
507     );
508
509     fieldmapper.standardRequest(
510         ['open-ils.acq', 'open-ils.acq.invoice.update'],
511         {
512             params : [openils.User.authtoken, invoice, updateEntries, updateItems],
513             oncomplete : function(r) {
514                 progressDialog.hide();
515                 var invoice = openils.Util.readResponse(r);
516                 if(invoice) {
517                     if(doProrate)
518                         return prorateInvoice(invoice);
519                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
520                 }
521             }
522         }
523     );
524 }
525
526 function prorateInvoice(invoice) {
527     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
528     progressDialog.show(true);
529
530     fieldmapper.standardRequest(
531         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
532         {
533             params : [openils.User.authtoken, invoice.id()],
534             oncomplete : function(r) {
535                 progressDialog.hide();
536                 var invoice = openils.Util.readResponse(r);
537                 if(invoice) {
538                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
539                 }
540             }
541         }
542     );
543
544 }
545
546
547
548 openils.Util.addOnLoad(init);
549
550