]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
Loosened some restrictions on invoicing. It is now possible to invoice
[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 dojo.require('openils.acq.Lineitem');
14
15 dojo.requireLocalization('openils.acq', 'acq');
16 var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
17
18 var fundLabelFormat = ['${0} (${1})', 'code', 'year'];
19 var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
20
21 var cgi = new openils.CGI();
22 var pcrud = new openils.PermaCrud();
23 var attachLi;
24 var attachPo;
25 var invoice;
26 var itemTbody;
27 var itemTemplate;
28 var entryTemplate;
29 var totalInvoicedBox;
30 var totalPaidBox;
31 var balanceOwedBox;
32 var invoicePane;
33 var itemTypes;
34 var virtualId = -1;
35 var extraCopies = {};
36 var extraCopiesFund;
37 var widgetRegistry = {acqie : {}, acqii : {}};
38
39 function nodeByName(name, context) {
40     return dojo.query('[name='+name+']', context)[0];
41 }
42
43 function init() {
44
45     attachLi = cgi.param('attach_li');
46     attachPo = cgi.param('attach_po');
47
48     itemTypes = pcrud.retrieveAll('aiit');
49
50     if(cgi.param('create')) {
51         renderInvoice();
52
53     } else {
54         fieldmapper.standardRequest(
55             ['open-ils.acq', 'open-ils.acq.invoice.retrieve'],
56             {
57                 params : [openils.User.authtoken, invoiceId],
58                 oncomplete : function(r) {
59                     invoice = openils.Util.readResponse(r);     
60                     renderInvoice();
61                 }
62             }
63         );
64     }
65
66     extraCopiesFund = new openils.widget.AutoFieldWidget({
67         fmField : 'fund',
68         fmClass : 'acqlid',
69         searchFilter : {active : 't'},
70         labelFormat : fundLabelFormat,
71         searchFormat : fundSearchFormat,
72         dijitArgs : {required : true},
73         parentNode : dojo.byId('acq-invoice-extra-copies-fund')
74     });
75     extraCopiesFund.build();
76 }
77
78 function renderInvoice() {
79
80     // in create mode, let the LI or PO render the invoice with seed data
81     if( !(cgi.param('create') && (attachPo || attachLi)) ) {
82         invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), invoice);
83     }
84
85     dojo.byId('acq-invoice-new-item').onclick = function() {
86         var item = new fieldmapper.acqii();
87         item.id(virtualId--);
88         item.isnew(true);
89         addInvoiceItem(item);
90     }
91
92     updateTotalCost();
93
94     if(invoice && openils.Util.isTrue(invoice.complete())) {
95
96         dojo.forEach( // hide widgets that should not be visible for a completed invoice
97             dojo.query('.hide-complete'), 
98             function(node) { openils.Util.hide(node); }
99         );
100
101         new openils.User().getPermOrgList(
102             'ACQ_INVOICE_REOPEN', 
103             function (orgs) {
104                 if(orgs.indexOf(invoice.receiver()) >= 0)
105                     openils.Util.show('acq-invoice-reopen-button-wrapper', 'inline');
106             }, 
107             true, 
108             true
109         );
110     }
111
112     if(invoice) {
113         dojo.forEach(
114             invoice.items(),
115             function(item) {
116                 addInvoiceItem(item);
117             }
118         );
119
120         dojo.forEach(
121             invoice.entries(),
122             function(entry) {
123                 addInvoiceEntry(entry);
124             }
125         );
126     }
127
128     if(attachLi) doAttachLi();
129     if(attachPo) doAttachPo();
130 }
131
132 function doAttachLi() {
133
134     //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; 
135     if(cgi.param('create')) {
136         var invoiceArgs = {};
137         invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
138     }
139     var entry = new fieldmapper.acqie();
140     entry.id(virtualId--);
141     entry.isnew(true);
142     entry.lineitem(attachLi);
143     addInvoiceEntry(entry);
144 }
145
146 function doAttachPo() {
147
148     fieldmapper.standardRequest(
149         ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
150         {   async: true,
151             params: [openils.User.authtoken, attachPo, {flesh_lineitem_ids : true}],
152             oncomplete: function(r) {
153                 var po = openils.Util.readResponse(r);
154
155                 if(cgi.param('create')) {
156                     // render the invoice using some seed data from the PO
157                     var invoiceArgs = {provider : po.provider(), shipper : po.provider()}; 
158                     invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
159                 }
160
161                 dojo.forEach(po.lineitems(), 
162                     function(lineitem) {
163                         var entry = new fieldmapper.acqie();
164                         entry.id(virtualId--);
165                         entry.isnew(true);
166                         entry.lineitem(lineitem);
167                         entry.purchase_order(po);
168                         addInvoiceEntry(entry);
169                     }
170                 );
171             }
172         }
173     );
174 }
175
176 function updateTotalCost() {
177
178     var totalCost = 0;    
179     for(var id in widgetRegistry.acqii) 
180         if(!widgetRegistry.acqii[id]._object.isdeleted())
181             totalCost += Number(widgetRegistry.acqii[id].cost_billed.getFormattedValue());
182     for(var id in widgetRegistry.acqie) 
183         if(!widgetRegistry.acqie[id]._object.isdeleted())
184             totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
185     totalInvoicedBox.attr('value', totalCost);
186
187     totalPaid = 0;    
188     for(var id in widgetRegistry.acqii) 
189         if(!widgetRegistry.acqii[id]._object.isdeleted())
190             totalPaid += Number(widgetRegistry.acqii[id].amount_paid.getFormattedValue());
191     for(var id in widgetRegistry.acqie) 
192         if(!widgetRegistry.acqie[id]._object.isdeleted())
193             totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
194     totalPaidBox.attr('value', totalPaid);
195
196     var buttonsDisabled = false;
197
198     if(totalPaid > totalCost || totalPaid < 0) {
199         openils.Util.addCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
200         invoiceSaveButton.attr('disabled', true);
201         invoiceProrateButton.attr('disabled', true);
202         buttonsDisabled = true;
203     } else {
204         openils.Util.removeCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
205         invoiceSaveButton.attr('disabled', false);
206         invoiceProrateButton.attr('disabled', false);
207     }
208
209     if(totalCost < 0) {
210         openils.Util.addCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
211         invoiceSaveButton.attr('disabled', true);
212         invoiceProrateButton.attr('disabled', true);
213     } else {
214         openils.Util.removeCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
215         if(!buttonsDisabled) {
216             invoiceSaveButton.attr('disabled', false);
217             invoiceProrateButton.attr('disabled', false);
218         }
219     }
220
221     if(totalPaid == totalCost) { // XXX: too rigid?
222         invoiceCloseButton.attr('disabled', false);
223     } else {
224         invoiceCloseButton.attr('disabled', true);
225     }
226
227     balanceOwedBox.attr('value', (totalCost - totalPaid));
228 }
229
230
231 function registerWidget(obj, field, widget, callback) {
232     var blob = widgetRegistry[obj.classname];
233     if(!blob[obj.id()]) 
234         blob[obj.id()] = {_object : obj};
235     blob[obj.id()][field] = widget;
236     widget.build(
237         function(w, ww) {
238             dojo.connect(w, 'onChange', 
239                 function(newVal) { 
240                     obj.ischanged(true); 
241                     updateTotalCost();
242                 }
243             );
244             if(callback) callback(w, ww);
245         }
246     );
247     return widget;
248 }
249
250 function addInvoiceItem(item) {
251     itemTbody = dojo.byId('acq-invoice-item-tbody');
252     if(itemTemplate == null) {
253         itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
254     }
255
256     var row = itemTemplate.cloneNode(true);
257     var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
258
259     dojo.forEach(
260         ['title', 'author', 'cost_billed', 'amount_paid'], 
261         function(field) {
262             
263             var args;
264             if(field == 'title' || field == 'author') {
265                 args = {style : 'width:10em'};
266             } else if(field == 'cost_billed' || field == 'amount_paid') {
267                 args = {required : true, style : 'width: 6em'};
268             }
269             registerWidget(
270                 item,
271                 field,
272                 new openils.widget.AutoFieldWidget({
273                     fmClass : 'acqii',
274                     fmObject : item,
275                     fmField : field,
276                     readOnly : invoice && openils.Util.isTrue(invoice.complete()),
277                     dijitArgs : args,
278                     parentNode : nodeByName(field, row)
279                 })
280             )
281         }
282     );
283
284
285     /* ----------- fund -------------- */
286     var fundArgs = {
287         fmClass : 'acqii',
288         fmObject : item,
289         fmField : 'fund',
290         labelFormat : fundLabelFormat,
291         searchFormat : fundSearchFormat,
292         readOnly : invoice && openils.Util.isTrue(invoice.complete()),
293         dijitArgs : {required : true},
294         parentNode : nodeByName('fund', row)
295     }
296
297     if(item.fund_debit()) {
298         fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
299     } else {
300         fundArgs.searchFilter = {active : 't'}
301         if(itemType && openils.Util.isTrue(itemType.prorate()))
302             fundArgs.dijitArgs = {disabled : true};
303     }
304
305     var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
306     registerWidget(item, 'fund', fundWidget);
307
308     /* ---------- inv_item_type ------------- */
309
310     registerWidget(
311         item,
312         'inv_item_type',
313         new openils.widget.AutoFieldWidget({
314             fmObject : item,
315             fmField : 'inv_item_type',
316             parentNode : nodeByName('inv_item_type', row),
317             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
318             dijitArgs : {required : true}
319         }),
320         function(w, ww) {
321             // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
322             // since this charge will be prorated against (potentially) multiple funds
323             dojo.connect(w, 'onChange', 
324                 function() {
325                     if(!item.fund_debit()) {
326                         var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
327                         if(!itemType) return;
328                         if(openils.Util.isTrue(itemType.prorate())) {
329                             fundWidget.widget.attr('disabled', true);
330                             fundWidget.widget.attr('value', '');
331                         } else {
332                             fundWidget.widget.attr('disabled', false);
333                         }
334                     }
335                 }
336             );
337         }
338     );
339
340     nodeByName('delete', row).onclick = function() {
341         var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
342         var msg = dojo.string.substitute(
343             localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
344                 cost,
345                 widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue()
346             ]
347         );
348         if(!confirm(msg)) return;
349         itemTbody.removeChild(row);
350         item.isdeleted(true);
351         if(item.isnew())
352             delete widgetRegistry.acqii[item.id()];
353         updateTotalCost();
354     }
355
356     itemTbody.appendChild(row);
357     updateTotalCost();
358 }
359
360 function addInvoiceEntry(entry) {
361
362     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
363     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
364     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
365
366     entryTbody = dojo.byId('acq-invoice-entry-tbody');
367     if(entryTemplate == null) {
368         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
369     }
370
371     if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
372         // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
373         return;
374
375     var row = entryTemplate.cloneNode(true);
376     row.setAttribute('lineitem', entry.lineitem());
377
378     openils.acq.Lineitem.fetchAndRender(
379         entry.lineitem(), {}, 
380         function(li, html) { 
381             entry.lineitem(li);
382             entry.purchase_order(li.purchase_order());
383             nodeByName('title_details', row).innerHTML = html;
384
385             dojo.forEach(
386                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
387                 function(field) {
388                     var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
389                     if(entry.isnew() && field == 'phys_item_count') {
390                         // by default, attempt to pay for all received and as-of-yet-un-invoiced items
391                         dijitArgs.value = (Number(li.order_summary().recv_count()) - Number(li.order_summary().invoice_count())) || 0;
392                     }
393                     registerWidget(
394                         entry, 
395                         field,
396                         new openils.widget.AutoFieldWidget({
397                             fmObject : entry,
398                             fmClass : 'acqie',
399                             fmField : field,
400                             dijitArgs : dijitArgs,
401                             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
402                             parentNode : nodeByName(field, row)
403                         }),
404                         function(w) {    
405                             if(field == 'phys_item_count') {
406                                 dojo.connect(w, 'onChange', 
407                                     function() {
408                                         // staff entered a higher number in the receive field than was originally ordered
409                                         // taking into account already invoiced items
410                                         var extra = Number(this.attr('value')) - 
411                                             (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
412                                         if(extra > 0) {
413                                             storeExtraCopies(entry, extra);
414                                         }
415                                     }
416                                 )
417                             }
418                         }
419                     );
420                 }
421             );
422         }
423     );
424
425     nodeByName('detach', row).onclick = function() {
426         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
427         var msg = dojo.string.substitute(
428             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
429                 cost || 0,
430                 liMarcAttr(lineitem, 'title'),
431                 liMarcAttr(lineitem, 'author'),
432                 idents.join(',')
433             ]
434         );
435         if(!confirm(msg)) return;
436         entryTbody.removeChild(row);
437         entry.isdeleted(true);
438         if(entry.isnew())
439             delete widgetRegistry.acqie[entry.id()];
440         updateTotalCost();
441     }
442
443     entryTbody.appendChild(row);
444     updateTotalCost();
445 }
446
447 function liMarcAttr(lineitem, name) {
448     var attr = lineitem.attributes().filter(
449         function(attr) { 
450             if(
451                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
452                 attr.attr_name() == name) 
453                     return attr 
454         } 
455     )[0];
456     return (attr) ? attr.attr_value() : '';
457 }
458
459 function saveChanges(doProrate, doClose, doReopen) {
460     createExtraCopies(
461         function() {
462             saveChangesPartTwo(doProrate, doClose, doReopen);
463         }
464     );
465 }
466
467 function saveChangesPartTwo(doProrate, doClose, doReopen) {
468     
469     progressDialog.show(true);
470
471     if(doReopen) {
472         invoice.complete('f');
473
474     } else {
475
476
477         var updateItems = [];
478         for(var id in widgetRegistry.acqii) {
479             var reg = widgetRegistry.acqii[id];
480             var item = reg._object;
481             if(item.ischanged() || item.isnew() || item.isdeleted()) {
482                 updateItems.push(item);
483                 if(item.isnew()) item.id(null);
484                 for(var field in reg) {
485                     if(field != '_object')
486                         item[field]( reg[field].getFormattedValue() );
487                 }
488                 
489                 // unflesh
490                 if(item.purchase_order() != null && typeof item.purchase_order() == 'object')
491                     item.purchase_order( item.purchase_order().id() );
492             }
493         }
494
495         var updateEntries = [];
496         for(var id in widgetRegistry.acqie) {
497             var reg = widgetRegistry.acqie[id];
498             var entry = reg._object;
499             if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
500                 entry.lineitem(entry.lineitem().id());
501                 entry.purchase_order(entry.purchase_order().id());
502                 updateEntries.push(entry);
503                 if(entry.isnew()) entry.id(null);
504
505                 for(var field in reg) {
506                     if(field != '_object')
507                         entry[field]( reg[field].getFormattedValue() );
508                 }
509                 
510                 // unflesh
511                 dojo.forEach(['purchase_order', 'lineitem'],
512                     function(field) {
513                         if(entry[field]() != null && typeof entry[field]() == 'object')
514                             entry[field]( entry[field]().id() );
515                     }
516                 );
517             }
518         }
519
520         if(!invoice) {
521             invoice = new fieldmapper.acqinv();
522             invoice.isnew(true);
523         } else {
524             invoice.ischanged(true); // for now, just always update
525         }
526
527         dojo.forEach(invoicePane.fieldList, 
528             function(field) {
529                 invoice[field.name]( field.widget.getFormattedValue() );
530             }
531         );
532
533         if(doClose) 
534             invoice.complete('t');
535     }
536
537     fieldmapper.standardRequest(
538         ['open-ils.acq', 'open-ils.acq.invoice.update'],
539         {
540             params : [openils.User.authtoken, invoice, updateEntries, updateItems],
541             oncomplete : function(r) {
542                 progressDialog.hide();
543                 var invoice = openils.Util.readResponse(r);
544                 if(invoice) {
545                     if(doProrate)
546                         return prorateInvoice(invoice);
547                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
548                 }
549             }
550         }
551     );
552 }
553
554 function prorateInvoice(invoice) {
555     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
556     progressDialog.show(true);
557
558     fieldmapper.standardRequest(
559         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
560         {
561             params : [openils.User.authtoken, invoice.id()],
562             oncomplete : function(r) {
563                 progressDialog.hide();
564                 var invoice = openils.Util.readResponse(r);
565                 if(invoice) {
566                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
567                 }
568             }
569         }
570     );
571 }
572
573 function storeExtraCopies(entry, numExtra) {
574
575     dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
576         dojo.string.substitute(
577             localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
578
579     var addCopyHandler;
580     addCopyHandler = dojo.connect(
581         extraCopiesGo, 
582         'onClick',
583         function() {
584             extraCopies[entry.lineitem().id()] = {
585                 numExtra : numExtra, 
586                 fund : extraCopiesFund.widget.attr('value')
587             }
588             extraItemsDialog.hide();
589             dojo.disconnect(addCopyHandler);
590         }
591     );
592
593     dojo.connect(
594         extraCopiesCancel, 
595         'onClick',
596         function() { 
597             widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
598             extraItemsDialog.hide() 
599         }
600     );
601
602     extraItemsDialog.show();
603 }
604
605 function createExtraCopies(oncomplete) {
606
607     var lids = [];
608     for(var liId in extraCopies) {
609         var data = extraCopies[liId];
610         for(var i = 0; i < data.numExtra; i++) {
611             var lid = new fieldmapper.acqlid();
612             lid.isnew(true);
613             lid.lineitem(liId);
614             lid.fund(data.fund);
615             lid.recv_time('now');
616             lids.push(lid);
617         }
618     }
619
620     if(lids.length == 0) 
621         return oncomplete();
622
623     fieldmapper.standardRequest(
624         ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
625         {
626             params : [openils.User.authtoken, lids, true],
627             oncomplete : function(r) {
628                 if(openils.Util.readResponse(r))
629                     oncomplete();
630             }
631         }
632     );
633
634 }
635
636
637 openils.Util.addOnLoad(init);
638
639