Initial dev repository
[kcls-web.git] / js / ui / default / acq / invoice / view.js
diff --git a/js/ui/default/acq/invoice/view.js b/js/ui/default/acq/invoice/view.js
new file mode 100644 (file)
index 0000000..281189f
--- /dev/null
@@ -0,0 +1,695 @@
+dojo.require('dojo.date.locale');
+dojo.require('dojo.date.stamp');
+dojo.require('dijit.form.CheckBox');
+dojo.require('dijit.form.CurrencyTextBox');
+dojo.require('dijit.form.NumberTextBox');
+dojo.require('openils.User');
+dojo.require('openils.Util');
+dojo.require('openils.CGI');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.EditPane');
+dojo.require('openils.widget.AutoFieldWidget');
+dojo.require('openils.widget.ProgressDialog');
+dojo.require('openils.acq.Lineitem');
+
+dojo.requireLocalization('openils.acq', 'acq');
+var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
+
+var fundLabelFormat = ['${0} (${1})', 'code', 'year'];
+var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
+
+var cgi = new openils.CGI();
+var pcrud = new openils.PermaCrud();
+var attachLi;
+var attachPo;
+var invoice;
+var itemTbody;
+var itemTemplate;
+var entryTemplate;
+var totalInvoicedBox;
+var totalPaidBox;
+var balanceOwedBox;
+var invoicePane;
+var itemTypes;
+var virtualId = -1;
+var extraCopies = {};
+var extraCopiesFund;
+var widgetRegistry = {acqie : {}, acqii : {}};
+
+function nodeByName(name, context) {
+    return dojo.query('[name='+name+']', context)[0];
+}
+
+function init() {
+
+    attachLi = cgi.param('attach_li');
+    attachPo = cgi.param('attach_po');
+
+    itemTypes = pcrud.retrieveAll('aiit');
+
+    if(cgi.param('create')) {
+        renderInvoice();
+
+    } else {
+        fieldmapper.standardRequest(
+            ['open-ils.acq', 'open-ils.acq.invoice.retrieve.authoritative'],
+            {
+                params : [openils.User.authtoken, invoiceId],
+                oncomplete : function(r) {
+                    invoice = openils.Util.readResponse(r);     
+                    renderInvoice();
+                }
+            }
+        );
+    }
+
+    extraCopiesFund = new openils.widget.AutoFieldWidget({
+        fmField : 'fund',
+        fmClass : 'acqlid',
+        searchFilter : {active : 't'},
+        labelFormat : fundLabelFormat,
+        searchFormat : fundSearchFormat,
+        dijitArgs : {required : true},
+        parentNode : dojo.byId('acq-invoice-extra-copies-fund')
+    });
+    extraCopiesFund.build();
+}
+
+function renderInvoice() {
+
+    // in create mode, let the LI or PO render the invoice with seed data
+    if( !(cgi.param('create') && (attachPo || attachLi)) ) {
+        invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), invoice);
+    }
+
+    dojo.byId('acq-invoice-new-item').onclick = function() {
+        var item = new fieldmapper.acqii();
+        item.id(virtualId--);
+        item.isnew(true);
+        addInvoiceItem(item);
+    }
+
+    updateTotalCost();
+
+    if(invoice && openils.Util.isTrue(invoice.complete())) {
+
+        dojo.forEach( // hide widgets that should not be visible for a completed invoice
+            dojo.query('.hide-complete'), 
+            function(node) { openils.Util.hide(node); }
+        );
+
+        new openils.User().getPermOrgList(
+            'ACQ_INVOICE_REOPEN', 
+            function (orgs) {
+                if(orgs.indexOf(invoice.receiver()) >= 0)
+                    openils.Util.show('acq-invoice-reopen-button-wrapper', 'inline');
+            }, 
+            true, 
+            true
+        );
+    }
+
+    if(invoice) {
+        dojo.forEach(
+            invoice.items(),
+            function(item) {
+                addInvoiceItem(item);
+            }
+        );
+
+        dojo.forEach(
+            invoice.entries(),
+            function(entry) {
+                addInvoiceEntry(entry);
+            }
+        );
+    }
+
+    if(attachLi) doAttachLi();
+    if(attachPo) doAttachPo();
+}
+
+function doAttachLi() {
+
+    //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; 
+    if(cgi.param('create')) {
+        var invoiceArgs = {};
+        invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
+    }
+    var entry = new fieldmapper.acqie();
+    entry.id(virtualId--);
+    entry.isnew(true);
+    entry.lineitem(attachLi);
+    addInvoiceEntry(entry);
+}
+
+function doAttachPo() {
+
+    fieldmapper.standardRequest(
+        ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
+        {   async: true,
+            params: [
+                openils.User.authtoken, attachPo, 
+                {flesh_lineitem_ids : true, flesh_po_items : true}
+            ],
+            oncomplete: function(r) {
+                var po = openils.Util.readResponse(r);
+
+                if(cgi.param('create')) {
+                    // render the invoice using some seed data from the PO
+                    var invoiceArgs = {provider : po.provider(), shipper : po.provider()}; 
+                    invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
+                }
+
+                dojo.forEach(po.lineitems(), 
+                    function(lineitem) {
+                        var entry = new fieldmapper.acqie();
+                        entry.id(virtualId--);
+                        entry.isnew(true);
+                        entry.lineitem(lineitem);
+                        entry.purchase_order(po);
+                        addInvoiceEntry(entry);
+                    }
+                );
+
+                dojo.forEach(po.po_items(),
+                    function(poItem) {
+                        var item = new fieldmapper.acqii();
+                        item.id(virtualId--);
+                        item.isnew(true);
+                        item.fund(poItem.fund());
+                        item.title(poItem.title());
+                        item.author(poItem.author());
+                        item.note(poItem.note());
+                        item.inv_item_type(poItem.inv_item_type());
+                        item.purchase_order(po);
+                        item.po_item(poItem);
+                        addInvoiceItem(item);
+                    }
+                );
+            }
+        }
+    );
+}
+
+function updateTotalCost() {
+
+    var totalCost = 0;    
+    for(var id in widgetRegistry.acqii) 
+        if(!widgetRegistry.acqii[id]._object.isdeleted())
+            totalCost += Number(widgetRegistry.acqii[id].cost_billed.getFormattedValue());
+    for(var id in widgetRegistry.acqie) 
+        if(!widgetRegistry.acqie[id]._object.isdeleted())
+            totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
+    totalInvoicedBox.attr('value', totalCost);
+
+    totalPaid = 0;    
+    for(var id in widgetRegistry.acqii) 
+        if(!widgetRegistry.acqii[id]._object.isdeleted())
+            totalPaid += Number(widgetRegistry.acqii[id].amount_paid.getFormattedValue());
+    for(var id in widgetRegistry.acqie) 
+        if(!widgetRegistry.acqie[id]._object.isdeleted())
+            totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
+    totalPaidBox.attr('value', totalPaid);
+
+    var buttonsDisabled = false;
+
+    if(totalPaid > totalCost || totalPaid < 0) {
+        openils.Util.addCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
+        invoiceSaveButton.attr('disabled', true);
+        invoiceProrateButton.attr('disabled', true);
+        buttonsDisabled = true;
+    } else {
+        openils.Util.removeCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
+        invoiceSaveButton.attr('disabled', false);
+        invoiceProrateButton.attr('disabled', false);
+    }
+
+    if(totalCost < 0) {
+        openils.Util.addCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
+        invoiceSaveButton.attr('disabled', true);
+        invoiceProrateButton.attr('disabled', true);
+    } else {
+        openils.Util.removeCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
+        if(!buttonsDisabled) {
+            invoiceSaveButton.attr('disabled', false);
+            invoiceProrateButton.attr('disabled', false);
+        }
+    }
+
+    if(totalPaid == totalCost) { // XXX: too rigid?
+        invoiceCloseButton.attr('disabled', false);
+    } else {
+        invoiceCloseButton.attr('disabled', true);
+    }
+
+    balanceOwedBox.attr('value', (totalCost - totalPaid));
+}
+
+
+function registerWidget(obj, field, widget, callback) {
+    var blob = widgetRegistry[obj.classname];
+    if(!blob[obj.id()]) 
+        blob[obj.id()] = {_object : obj};
+    blob[obj.id()][field] = widget;
+    widget.build(
+        function(w, ww) {
+            dojo.connect(w, 'onChange', 
+                function(newVal) { 
+                    obj.ischanged(true); 
+                    updateTotalCost();
+                }
+            );
+            if(callback) callback(w, ww);
+        }
+    );
+    return widget;
+}
+
+function addInvoiceItem(item) {
+    itemTbody = dojo.byId('acq-invoice-item-tbody');
+    if(itemTemplate == null) {
+        itemTemplate = itemTbody.removeChild(dojo.byId('acq-invoice-item-template'));
+    }
+
+    var row = itemTemplate.cloneNode(true);
+    var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
+
+    dojo.forEach(
+        ['title', 'author', 'cost_billed', 'amount_paid'], 
+        function(field) {
+            
+            var args;
+            if(field == 'title' || field == 'author') {
+                //args = {style : 'width:10em'};
+            } else if(field == 'cost_billed' || field == 'amount_paid') {
+                args = {required : true, style : 'width: 6em'};
+            }
+            registerWidget(
+                item,
+                field,
+                new openils.widget.AutoFieldWidget({
+                    fmClass : 'acqii',
+                    fmObject : item,
+                    fmField : field,
+                    readOnly : invoice && openils.Util.isTrue(invoice.complete()),
+                    dijitArgs : args,
+                    parentNode : nodeByName(field, row)
+                })
+            )
+        }
+    );
+
+
+    /* ----------- fund -------------- */
+    var fundArgs = {
+        fmClass : 'acqii',
+        fmObject : item,
+        fmField : 'fund',
+        labelFormat : fundLabelFormat,
+        searchFormat : fundSearchFormat,
+        readOnly : invoice && openils.Util.isTrue(invoice.complete()),
+        dijitArgs : {required : true},
+        parentNode : nodeByName('fund', row)
+    }
+
+    if(item.fund_debit()) {
+        fundArgs.searchFilter = {'-or' : [{active : 't'}, {id : item.fund()}]};
+    } else {
+        fundArgs.searchFilter = {active : 't'}
+        if(itemType && openils.Util.isTrue(itemType.prorate()))
+            fundArgs.dijitArgs = {disabled : true};
+    }
+
+    var fundWidget = new openils.widget.AutoFieldWidget(fundArgs);
+    registerWidget(item, 'fund', fundWidget);
+
+    /* ---------- inv_item_type ------------- */
+
+    if(item.po_item()) {
+
+        // read-only item view for items that were the result of a po-item
+        var po = item.purchase_order();
+        var po_item = item.po_item();
+        var node = nodeByName('inv_item_type', row);
+        var itemType = itemTypes.filter(function(t) { return (t.code() == item.inv_item_type()) })[0];
+        orderDate = (!po.order_date()) ? '' : 
+                dojo.date.locale.format(dojo.date.stamp.fromISOString(po.order_date()), {selector:'date'});
+
+        node.innerHTML = dojo.string.substitute(
+            localeStrings.INVOICE_ITEM_PO_DETAILS, 
+            [ 
+                itemType.name(),
+                oilsBasePath, 
+                po.id(), 
+                po.name(), 
+                orderDate,
+                po_item.estimated_cost() 
+            ]
+        );
+
+    } else {
+
+        registerWidget(
+            item,
+            'inv_item_type',
+            new openils.widget.AutoFieldWidget({
+                fmObject : item,
+                fmField : 'inv_item_type',
+                parentNode : nodeByName('inv_item_type', row),
+                readOnly : invoice && openils.Util.isTrue(invoice.complete()),
+                dijitArgs : {required : true}
+            }),
+            function(w, ww) {
+                // When the inv_item_type is set to prorate=true, don't allow the user the edit the fund
+                // since this charge will be prorated against (potentially) multiple funds
+                dojo.connect(w, 'onChange', 
+                    function() {
+                        if(!item.fund_debit()) {
+                            var itemType = itemTypes.filter(function(t) { return (t.code() == w.attr('value')) })[0];
+                            if(!itemType) return;
+                            if(openils.Util.isTrue(itemType.prorate())) {
+                                fundWidget.widget.attr('disabled', true);
+                                fundWidget.widget.attr('value', '');
+                            } else {
+                                fundWidget.widget.attr('disabled', false);
+                            }
+                        }
+                    }
+                );
+            }
+        );
+    }
+
+    nodeByName('delete', row).onclick = function() {
+        var cost = widgetRegistry.acqii[item.id()].cost_billed.getFormattedValue();
+        var msg = dojo.string.substitute(
+            localeStrings.INVOICE_CONFIRM_ITEM_DELETE, [
+                cost,
+                widgetRegistry.acqii[item.id()].inv_item_type.getFormattedValue()
+            ]
+        );
+        if(!confirm(msg)) return;
+        itemTbody.removeChild(row);
+        item.isdeleted(true);
+        if(item.isnew())
+            delete widgetRegistry.acqii[item.id()];
+        updateTotalCost();
+    }
+
+    itemTbody.appendChild(row);
+    updateTotalCost();
+}
+
+function addInvoiceEntry(entry) {
+
+    openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
+    openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
+    openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
+
+    entryTbody = dojo.byId('acq-invoice-entry-tbody');
+    if(entryTemplate == null) {
+        entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
+    }
+
+    if(dojo.query('[lineitem=' + entry.lineitem() +']', entryTbody)[0])
+        // Is it ever valid to have multiple entries for 1 lineitem in a single invoice?
+        return;
+
+    var row = entryTemplate.cloneNode(true);
+    row.setAttribute('lineitem', entry.lineitem());
+
+    openils.acq.Lineitem.fetchAndRender(
+        entry.lineitem(), {}, 
+        function(li, html) { 
+            entry.lineitem(li);
+            entry.purchase_order(li.purchase_order());
+            nodeByName('title_details', row).innerHTML = html;
+
+            dojo.forEach(
+                ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
+                function(field) {
+                    var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
+                    if(entry.isnew() && field == 'phys_item_count') {
+                        // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
+                        var count = Number(li.order_summary().item_count() || 0) - 
+                                    Number(li.order_summary().cancel_count() || 0) -
+                                    Number(li.order_summary().invoice_count() || 0);
+                        if(count < 0) count = 0;
+                        dijitArgs.value = count;
+                    }
+                    registerWidget(
+                        entry, 
+                        field,
+                        new openils.widget.AutoFieldWidget({
+                            fmObject : entry,
+                            fmClass : 'acqie',
+                            fmField : field,
+                            dijitArgs : dijitArgs,
+                            readOnly : invoice && openils.Util.isTrue(invoice.complete()),
+                            parentNode : nodeByName(field, row)
+                        }),
+                        function(w) {    
+                            if(field == 'phys_item_count') {
+                                dojo.connect(w, 'onChange', 
+                                    function() {
+                                        // staff entered a higher number in the receive field than was originally ordered
+                                        // taking into account already invoiced items
+                                        var extra = Number(this.attr('value')) - 
+                                            (Number(entry.lineitem().item_count()) - Number(entry.lineitem().order_summary().invoice_count()));
+                                        if(extra > 0) {
+                                            storeExtraCopies(entry, extra);
+                                        }
+                                    }
+                                )
+                            }
+                        }
+                    );
+                }
+            );
+        }
+    );
+
+    nodeByName('detach', row).onclick = function() {
+        var cost = widgetRegistry.acqie[entry.id()].cost_billed.getFormattedValue();
+        var idents = [];
+        dojo.forEach(['isbn', 'upc', 'issn'], 
+            function(ident) { 
+                var val = liMarcAttr(entry.lineitem(), ident);
+                if(val) idents.push(val); 
+            }
+        );
+
+        var msg = dojo.string.substitute(
+            localeStrings.INVOICE_CONFIRM_ENTRY_DETACH, [
+                cost || 0,
+                liMarcAttr(entry.lineitem(), 'title'),
+                liMarcAttr(entry.lineitem(), 'author'),
+                idents.join(',')
+            ]
+        );
+        if(!confirm(msg)) return;
+        entryTbody.removeChild(row);
+        entry.isdeleted(true);
+        if(entry.isnew())
+            delete widgetRegistry.acqie[entry.id()];
+        updateTotalCost();
+    }
+
+    entryTbody.appendChild(row);
+    updateTotalCost();
+}
+
+function liMarcAttr(lineitem, name) {
+    var attr = lineitem.attributes().filter(
+        function(attr) { 
+            if(
+                attr.attr_type() == 'lineitem_marc_attr_definition' && 
+                attr.attr_name() == name) 
+                    return attr 
+        } 
+    )[0];
+    return (attr) ? attr.attr_value() : '';
+}
+
+function saveChanges(doProrate, doClose, doReopen) {
+    createExtraCopies(
+        function() {
+            saveChangesPartTwo(doProrate, doClose, doReopen);
+        }
+    );
+}
+
+function saveChangesPartTwo(doProrate, doClose, doReopen) {
+    
+    progressDialog.show(true);
+
+    if(doReopen) {
+        invoice.complete('f');
+
+    } else {
+
+
+        var updateItems = [];
+        for(var id in widgetRegistry.acqii) {
+            var reg = widgetRegistry.acqii[id];
+            var item = reg._object;
+            if(item.ischanged() || item.isnew() || item.isdeleted()) {
+                updateItems.push(item);
+                if(item.isnew()) item.id(null);
+                for(var field in reg) {
+                    if(field != '_object')
+                        item[field]( reg[field].getFormattedValue() );
+                }
+                
+                // unflesh
+                if(item.purchase_order() != null && typeof item.purchase_order() == 'object')
+                    item.purchase_order( item.purchase_order().id() );
+            }
+        }
+
+        var updateEntries = [];
+        for(var id in widgetRegistry.acqie) {
+            var reg = widgetRegistry.acqie[id];
+            var entry = reg._object;
+            if(entry.ischanged() || entry.isnew() || entry.isdeleted()) {
+                entry.lineitem(entry.lineitem().id());
+                entry.purchase_order(entry.purchase_order().id());
+                updateEntries.push(entry);
+                if(entry.isnew()) entry.id(null);
+
+                for(var field in reg) {
+                    if(field != '_object')
+                        entry[field]( reg[field].getFormattedValue() );
+                }
+                
+                // unflesh
+                dojo.forEach(['purchase_order', 'lineitem'],
+                    function(field) {
+                        if(entry[field]() != null && typeof entry[field]() == 'object')
+                            entry[field]( entry[field]().id() );
+                    }
+                );
+            }
+        }
+
+        if(!invoice) {
+            invoice = new fieldmapper.acqinv();
+            invoice.isnew(true);
+        } else {
+            invoice.ischanged(true); // for now, just always update
+        }
+
+        dojo.forEach(invoicePane.fieldList, 
+            function(field) {
+                invoice[field.name]( field.widget.getFormattedValue() );
+            }
+        );
+
+        if(doClose) 
+            invoice.complete('t');
+    }
+
+    fieldmapper.standardRequest(
+        ['open-ils.acq', 'open-ils.acq.invoice.update'],
+        {
+            params : [openils.User.authtoken, invoice, updateEntries, updateItems],
+            oncomplete : function(r) {
+                progressDialog.hide();
+                var invoice = openils.Util.readResponse(r);
+                if(invoice) {
+                    if(doProrate)
+                        return prorateInvoice(invoice);
+                    location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
+                }
+            }
+        }
+    );
+}
+
+function prorateInvoice(invoice) {
+    if(!confirm(localeStrings.INVOICE_CONFIRM_PRORATE)) return;
+    progressDialog.show(true);
+
+    fieldmapper.standardRequest(
+        ['open-ils.acq', 'open-ils.acq.invoice.apply_prorate'],
+        {
+            params : [openils.User.authtoken, invoice.id()],
+            oncomplete : function(r) {
+                progressDialog.hide();
+                var invoice = openils.Util.readResponse(r);
+                if(invoice) {
+                    location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
+                }
+            }
+        }
+    );
+}
+
+function storeExtraCopies(entry, numExtra) {
+
+    dojo.byId('acq-invoice-extra-copies-message').innerHTML = 
+        dojo.string.substitute(
+            localeStrings.INVOICE_EXTRA_COPIES, [numExtra]);
+
+    var addCopyHandler;
+    addCopyHandler = dojo.connect(
+        extraCopiesGo, 
+        'onClick',
+        function() {
+            extraCopies[entry.lineitem().id()] = {
+                numExtra : numExtra, 
+                fund : extraCopiesFund.widget.attr('value')
+            }
+            extraItemsDialog.hide();
+            dojo.disconnect(addCopyHandler);
+        }
+    );
+
+    dojo.connect(
+        extraCopiesCancel, 
+        'onClick',
+        function() { 
+            widgetRegistry.acqie[entry.id()].phys_item_count.widget.attr('value', '');
+            extraItemsDialog.hide() 
+        }
+    );
+
+    extraItemsDialog.show();
+}
+
+function createExtraCopies(oncomplete) {
+
+    var lids = [];
+    for(var liId in extraCopies) {
+        var data = extraCopies[liId];
+        for(var i = 0; i < data.numExtra; i++) {
+            var lid = new fieldmapper.acqlid();
+            lid.isnew(true);
+            lid.lineitem(liId);
+            lid.fund(data.fund);
+            lid.recv_time('now');
+            lids.push(lid);
+        }
+    }
+
+    if(lids.length == 0) 
+        return oncomplete();
+
+    fieldmapper.standardRequest(
+        ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
+        {
+            params : [openils.User.authtoken, lids, true],
+            oncomplete : function(r) {
+                if(openils.Util.readResponse(r))
+                    oncomplete();
+            }
+        }
+    );
+
+}
+
+
+openils.Util.addOnLoad(init);
+
+