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