]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/invoice/view.js
38465b99e88849374f645ae03839c9f8a72e5226
[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.authoritative'],
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: [
152                 openils.User.authtoken, attachPo, 
153                 {flesh_lineitem_ids : true, flesh_po_items : true}
154             ],
155             oncomplete: function(r) {
156                 var po = openils.Util.readResponse(r);
157
158                 if(cgi.param('create')) {
159                     // render the invoice using some seed data from the PO
160                     var invoiceArgs = {provider : po.provider(), shipper : po.provider()}; 
161                     invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
162                 }
163
164                 dojo.forEach(po.lineitems(), 
165                     function(lineitem) {
166                         var entry = new fieldmapper.acqie();
167                         entry.id(virtualId--);
168                         entry.isnew(true);
169                         entry.lineitem(lineitem);
170                         entry.purchase_order(po);
171                         addInvoiceEntry(entry);
172                     }
173                 );
174
175                 dojo.forEach(po.po_items(),
176                     function(poItem) {
177                         var item = new fieldmapper.acqii();
178                         item.id(virtualId--);
179                         item.isnew(true);
180                         item.fund(poItem.fund());
181                         item.title(poItem.title());
182                         item.author(poItem.author());
183                         item.note(poItem.note());
184                         item.inv_item_type(poItem.inv_item_type());
185                         item.purchase_order(po);
186                         item.po_item(poItem);
187                         addInvoiceItem(item);
188                     }
189                 );
190             }
191         }
192     );
193 }
194
195 function updateTotalCost() {
196
197     var totalCost = 0;    
198     for(var id in widgetRegistry.acqii) 
199         if(!widgetRegistry.acqii[id]._object.isdeleted())
200             totalCost += Number(widgetRegistry.acqii[id].cost_billed.getFormattedValue());
201     for(var id in widgetRegistry.acqie) 
202         if(!widgetRegistry.acqie[id]._object.isdeleted())
203             totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
204     totalInvoicedBox.attr('value', totalCost);
205
206     totalPaid = 0;    
207     for(var id in widgetRegistry.acqii) 
208         if(!widgetRegistry.acqii[id]._object.isdeleted())
209             totalPaid += Number(widgetRegistry.acqii[id].amount_paid.getFormattedValue());
210     for(var id in widgetRegistry.acqie) 
211         if(!widgetRegistry.acqie[id]._object.isdeleted())
212             totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
213     totalPaidBox.attr('value', totalPaid);
214
215     var buttonsDisabled = false;
216
217     if(totalPaid > totalCost || totalPaid < 0) {
218         openils.Util.addCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
219         invoiceSaveButton.attr('disabled', true);
220         invoiceProrateButton.attr('disabled', true);
221         buttonsDisabled = true;
222     } else {
223         openils.Util.removeCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
224         invoiceSaveButton.attr('disabled', false);
225         invoiceProrateButton.attr('disabled', false);
226     }
227
228     if(totalCost < 0) {
229         openils.Util.addCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
230         invoiceSaveButton.attr('disabled', true);
231         invoiceProrateButton.attr('disabled', true);
232     } else {
233         openils.Util.removeCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
234         if(!buttonsDisabled) {
235             invoiceSaveButton.attr('disabled', false);
236             invoiceProrateButton.attr('disabled', false);
237         }
238     }
239
240     if(totalPaid == totalCost) { // XXX: too rigid?
241         invoiceCloseButton.attr('disabled', false);
242     } else {
243         invoiceCloseButton.attr('disabled', true);
244     }
245
246     balanceOwedBox.attr('value', (totalCost - totalPaid));
247 }
248
249
250 function registerWidget(obj, field, widget, callback) {
251     var blob = widgetRegistry[obj.classname];
252     if(!blob[obj.id()]) 
253         blob[obj.id()] = {_object : obj};
254     blob[obj.id()][field] = widget;
255     widget.build(
256         function(w, ww) {
257             dojo.connect(w, 'onChange', 
258                 function(newVal) { 
259                     obj.ischanged(true); 
260                     updateTotalCost();
261                 }
262             );
263             if(callback) callback(w, ww);
264         }
265     );
266     return widget;
267 }
268
269 function addInvoiceItem(item) {
270     itemTbody = dojo.byId('acq-invoice-item-tbody');
271     if(itemTemplate == null) {
272         itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
273     }
274
275     var row = itemTemplate.cloneNode(true);
276     var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
277
278     dojo.forEach(
279         ['title', 'author', 'cost_billed', 'amount_paid'], 
280         function(field) {
281             
282             var args;
283             if(field == 'title' || field == 'author') {
284                 //args = {style : 'width:10em'};
285             } else if(field == 'cost_billed' || field == 'amount_paid') {
286                 args = {required : true, style : 'width: 6em'};
287             }
288             registerWidget(
289                 item,
290                 field,
291                 new openils.widget.AutoFieldWidget({
292                     fmClass : 'acqii',
293                     fmObject : item,
294                     fmField : field,
295                     readOnly : invoice && openils.Util.isTrue(invoice.complete()),
296                     dijitArgs : args,
297                     parentNode : nodeByName(field, row)
298                 })
299             )
300         }
301     );
302
303
304     /* ----------- fund -------------- */
305     var fundArgs = {
306         fmClass : 'acqii',
307         fmObject : item,
308         fmField : 'fund',
309         labelFormat : fundLabelFormat,
310         searchFormat : fundSearchFormat,
311         readOnly : invoice && openils.Util.isTrue(invoice.complete()),
312         dijitArgs : {required : true},
313         parentNode : nodeByName('fund', row)
314     }
315
316     if(item.fund_debit()) {
317         fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
318     } else {
319         fundArgs.searchFilter = {active : 't'}
320         if(itemType && openils.Util.isTrue(itemType.prorate()))
321             fundArgs.dijitArgs = {disabled : true};
322     }
323
324     var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
325     registerWidget(item, 'fund', fundWidget);
326
327     /* ---------- inv_item_type ------------- */
328
329     if(item.po_item()) {
330
331         // read-only item view for items that were the result of a po-item
332         var po = item.purchase_order();
333         var po_item = item.po_item();
334         var node = nodeByName('inv_item_type', row);
335         var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
336         orderDate = (!po.order_date()) ? '' : 
337                 dojo.date.locale.format(dojo.date.stamp.fromISOString(po.order_date()), {selector:'date'});
338
339         node.innerHTML = dojo.string.substitute(
340             localeStrings.INVOICE_ITEM_PO_DETAILS, 
341             [ 
342                 itemType.name(),
343                 oilsBasePath, 
344                 po.id(), 
345                 po.name(), 
346                 orderDate,
347                 po_item.estimated_cost() 
348             ]
349         );
350
351     } else {
352
353         registerWidget(
354             item,
355             'inv_item_type',
356             new openils.widget.AutoFieldWidget({
357                 fmObject : item,
358                 fmField : 'inv_item_type',
359                 parentNode : nodeByName('inv_item_type', row),
360                 readOnly : invoice && openils.Util.isTrue(invoice.complete()),
361                 dijitArgs : {required : true}
362             }),
363             function(w, ww) {
364                 // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
365                 // since this charge will be prorated against (potentially) multiple funds
366                 dojo.connect(w, 'onChange', 
367                     function() {
368                         if(!item.fund_debit()) {
369                             var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
370                             if(!itemType) return;
371                             if(openils.Util.isTrue(itemType.prorate())) {
372                                 fundWidget.widget.attr('disabled', true);
373                                 fundWidget.widget.attr('value', '');
374                             } else {
375                                 fundWidget.widget.attr('disabled', false);
376                             }
377                         }
378                     }
379                 );
380             }
381         );
382     }
383
384     nodeByName('delete', row).onclick = function() {
385         var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
386         var msg = dojo.string.substitute(
387             localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
388                 cost || 0,
389                 widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue() || ''
390             ]
391         );
392         if(!confirm(msg)) return;
393         itemTbody.removeChild(row);
394         item.isdeleted(true);
395         if(item.isnew())
396             delete widgetRegistry.acqii[item.id()];
397         updateTotalCost();
398     }
399
400     itemTbody.appendChild(row);
401     updateTotalCost();
402 }
403
404 function addInvoiceEntry(entry) {
405
406     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
407     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
408     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
409
410     entryTbody = dojo.byId('acq-invoice-entry-tbody');
411     if(entryTemplate == null) {
412         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
413     }
414
415     if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
416         // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
417         return;
418
419     var row = entryTemplate.cloneNode(true);
420     row.setAttribute('lineitem', entry.lineitem());
421
422     openils.acq.Lineitem.fetchAndRender(
423         entry.lineitem(), {}, 
424         function(li, html) { 
425             entry.lineitem(li);
426             entry.purchase_order(li.purchase_order());
427             nodeByName('title_details', row).innerHTML = html;
428
429             dojo.forEach(
430                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
431                 function(field) {
432                     var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
433                     if(entry.isnew() && field == 'phys_item_count') {
434                         // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
435                         var count = Number(li.order_summary().item_count() || 0) - 
436                                     Number(li.order_summary().cancel_count() || 0) -
437                                     Number(li.order_summary().invoice_count() || 0);
438                         if(count < 0) count = 0;
439                         dijitArgs.value = count;
440                     }
441                     registerWidget(
442                         entry, 
443                         field,
444                         new openils.widget.AutoFieldWidget({
445                             fmObject : entry,
446                             fmClass : 'acqie',
447                             fmField : field,
448                             dijitArgs : dijitArgs,
449                             readOnly : invoice && openils.Util.isTrue(invoice.complete()),
450                             parentNode : nodeByName(field, row)
451                         }),
452                         function(w) {    
453                             if(field == 'phys_item_count') {
454                                 dojo.connect(w, 'onChange', 
455                                     function() {
456                                         // staff entered a higher number in the receive field than was originally ordered
457                                         // taking into account already invoiced items
458                                         var extra = Number(this.attr('value')) - 
459                                             (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
460                                         if(extra > 0) {
461                                             storeExtraCopies(entry, extra);
462                                         }
463                                     }
464                                 )
465                             }
466                         }
467                     );
468                 }
469             );
470         }
471     );
472
473     nodeByName('detach', row).onclick = function() {
474         var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
475         var idents = [];
476         dojo.forEach(['isbn', 'upc', 'issn'], 
477             function(ident) { 
478                 var val = liMarcAttr(entry.lineitem(), ident);
479                 if(val) idents.push(val); 
480             }
481         );
482
483         var msg = dojo.string.substitute(
484             localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
485                 cost || 0,
486                 liMarcAttr(entry.lineitem(), 'title'),
487                 liMarcAttr(entry.lineitem(), 'author'),
488                 idents.join(',')
489             ]
490         );
491         if(!confirm(msg)) return;
492         entryTbody.removeChild(row);
493         entry.isdeleted(true);
494         if(entry.isnew())
495             delete widgetRegistry.acqie[entry.id()];
496         updateTotalCost();
497     }
498
499     entryTbody.appendChild(row);
500     updateTotalCost();
501 }
502
503 function liMarcAttr(lineitem, name) {
504     var attr = lineitem.attributes().filter(
505         function(attr) { 
506             if(
507                 attr.attr_type() == 'lineitem_marc_attr_definition' && 
508                 attr.attr_name() == name) 
509                     return attr 
510         } 
511     )[0];
512     return (attr) ? attr.attr_value() : '';
513 }
514
515 function saveChanges(doProrate, doClose, doReopen) {
516     createExtraCopies(
517         function() {
518             saveChangesPartTwo(doProrate, doClose, doReopen);
519         }
520     );
521 }
522
523 function saveChangesPartTwo(doProrate, doClose, doReopen) {
524     
525     progressDialog.show(true);
526
527     if(doReopen) {
528         invoice.complete('f');
529
530     } else {
531
532
533         var updateItems = [];
534         for(var id in widgetRegistry.acqii) {
535             var reg = widgetRegistry.acqii[id];
536             var item = reg._object;
537             if(item.ischanged() || item.isnew() || item.isdeleted()) {
538                 updateItems.push(item);
539                 if(item.isnew()) item.id(null);
540                 for(var field in reg) {
541                     if(field != '_object')
542                         item[field]( reg[field].getFormattedValue() );
543                 }
544                 
545                 // unflesh
546                 if(item.purchase_order() != null && typeof item.purchase_order() == 'object')
547                     item.purchase_order( item.purchase_order().id() );
548             }
549         }
550
551         var updateEntries = [];
552         for(var id in widgetRegistry.acqie) {
553             var reg = widgetRegistry.acqie[id];
554             var entry = reg._object;
555             if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
556                 entry.lineitem(entry.lineitem().id());
557                 entry.purchase_order(entry.purchase_order().id());
558                 updateEntries.push(entry);
559                 if(entry.isnew()) entry.id(null);
560
561                 for(var field in reg) {
562                     if(field != '_object')
563                         entry[field]( reg[field].getFormattedValue() );
564                 }
565                 
566                 // unflesh
567                 dojo.forEach(['purchase_order', 'lineitem'],
568                     function(field) {
569                         if(entry[field]() != null && typeof entry[field]() == 'object')
570                             entry[field]( entry[field]().id() );
571                     }
572                 );
573             }
574         }
575
576         if(!invoice) {
577             invoice = new fieldmapper.acqinv();
578             invoice.isnew(true);
579         } else {
580             invoice.ischanged(true); // for now, just always update
581         }
582
583         dojo.forEach(invoicePane.fieldList, 
584             function(field) {
585                 invoice[field.name]( field.widget.getFormattedValue() );
586             }
587         );
588
589         if(doClose) 
590             invoice.complete('t');
591     }
592
593     fieldmapper.standardRequest(
594         ['open-ils.acq', 'open-ils.acq.invoice.update'],
595         {
596             params : [openils.User.authtoken, invoice, updateEntries, updateItems],
597             oncomplete : function(r) {
598                 progressDialog.hide();
599                 var invoice = openils.Util.readResponse(r);
600                 if(invoice) {
601                     if(doProrate)
602                         return prorateInvoice(invoice);
603                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
604                 }
605             }
606         }
607     );
608 }
609
610 function prorateInvoice(invoice) {
611     if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
612     progressDialog.show(true);
613
614     fieldmapper.standardRequest(
615         ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
616         {
617             params : [openils.User.authtoken, invoice.id()],
618             oncomplete : function(r) {
619                 progressDialog.hide();
620                 var invoice = openils.Util.readResponse(r);
621                 if(invoice) {
622                     location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
623                 }
624             }
625         }
626     );
627 }
628
629 function storeExtraCopies(entry, numExtra) {
630
631     dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
632         dojo.string.substitute(
633             localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
634
635     var addCopyHandler;
636     addCopyHandler = dojo.connect(
637         extraCopiesGo, 
638         'onClick',
639         function() {
640             extraCopies[entry.lineitem().id()] = {
641                 numExtra : numExtra, 
642                 fund : extraCopiesFund.widget.attr('value')
643             }
644             extraItemsDialog.hide();
645             dojo.disconnect(addCopyHandler);
646         }
647     );
648
649     dojo.connect(
650         extraCopiesCancel, 
651         'onClick',
652         function() { 
653             widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
654             extraItemsDialog.hide() 
655         }
656     );
657
658     extraItemsDialog.show();
659 }
660
661 function createExtraCopies(oncomplete) {
662
663     var lids = [];
664     for(var liId in extraCopies) {
665         var data = extraCopies[liId];
666         for(var i = 0; i < data.numExtra; i++) {
667             var lid = new fieldmapper.acqlid();
668             lid.isnew(true);
669             lid.lineitem(liId);
670             lid.fund(data.fund);
671             lid.recv_time('now');
672             lids.push(lid);
673         }
674     }
675
676     if(lids.length == 0) 
677         return oncomplete();
678
679     fieldmapper.standardRequest(
680         ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
681         {
682             params : [openils.User.authtoken, lids, true],
683             oncomplete : function(r) {
684                 if(openils.Util.readResponse(r))
685                     oncomplete();
686             }
687         }
688     );
689
690 }
691
692
693 openils.Util.addOnLoad(init);
694
695