Browse Source

本期合同计量公式1.0

MaiXinRong 1 year ago
parent
commit
526f39a318

+ 5 - 0
app/controller/stage_controller.js

@@ -273,6 +273,7 @@ module.exports = app => {
             const settleStatus = ctx.stage.readySettle ? await ctx.service.settleBills.getAllDataByCondition({ where: { settle_id: ctx.stage.readySettle.id }}) : [];
             // 查询截止上期数据
             const preStageData = ctx.stage.preCheckedStage ? await ctx.service.stageBillsFinal.getFinalData(ctx.tender.data, ctx.stage.preCheckedStage.order) : [];
+            const exprData = !ctx.stage.readOnly ? await ctx.service.expr.getAllDataByCondition({ where: { tid: ctx.stage.tid, calc_module: 'stage' } }) : [];
             this.ctx.helper.assignRelaData(ledgerData, [
                 { data: dgnData, fields: ['deal_dgn_qty1', 'deal_dgn_qty2', 'c_dgn_qty1', 'c_dgn_qty2'], prefix: '', relaId: 'id' },
                 { data: memoData, fields: this.ledgerMemoColumn, prefix: '', relaId: 'id' },
@@ -283,6 +284,7 @@ module.exports = app => {
                 { data: pcData, fields: ['contract_pc_tp', 'qc_pc_tp', 'pc_tp', 'org_price'], prefix: '', relaId: 'lid' },
                 { data: changeData, fields: ['qc_qty', 'qc_tp', 'qc_minus_qty'], prefix: 'due_', relaId: 'gcl_id' },
                 { data: settleStatus, fields: ['settle_status'], prefix: '', relaId: 'lid' },
+                { data: exprData, fields: ['expr'], prefix: 'calc_', relaId: 'calc_id' },
             ]);
             return ledgerData;
         }
@@ -389,6 +391,9 @@ module.exports = app => {
                             spec.zlj.deal_bills_tp = ctx.tender.info.deal_param.zanLiePrice;
                             responseData.data.spec = spec;
                             break;
+                        case 'expr':
+                            responseData.data.expr = exprData;
+                            break;
                     }
                 }
 

+ 24 - 0
app/controller/tender_controller.js

@@ -1604,6 +1604,30 @@ module.exports = app => {
             }
             ctx.redirect(ctx.request.header.referer);
         }
+
+        async saveExpr(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.calc_module || !data.calc_tag || !data.calc_id) throw '保存计算式参数有误';
+                await this.ctx.service.expr.saveExpr(ctx.tender.id, data);
+                ctx.body = { err: 0, msg: '', data: data.expr };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '保存计算式失败');
+            }
+        }
+
+        async loadExpr(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.calc_module || !data.calc_tag || !data.calc_id) throw '获取计算式参数有误';
+                const expr = await this.ctx.service.expr.loadExpr(ctx.tender.id, data);
+                ctx.body = { err: 0, msg: '', data: expr };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '保存计算式失败');
+            }
+        }
     }
 
     return TenderController;

+ 208 - 1
app/public/js/ledger.js

@@ -1210,7 +1210,7 @@ $(document).ready(function() {
         });
     }
 
-    let batchInsertObj;
+    let batchInsertObj, contractExprObj;
     $.contextMenu.types.batchInsert = function (item, opt, root) {
         const self = this;
         if ($.isFunction(item.icon)) {
@@ -1704,6 +1704,24 @@ $(document).ready(function() {
             return is_debug;
         }
     };
+    if (contractExpr && readOnly) {
+        billsContextMenuOptions.items.exprTag = '----';
+        billsContextMenuOptions.items.contractExpr = {
+            name: '预设本期合同计量公式',
+            callback: function (key, opt, menu, e) {
+                const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                if (!contractExprObj) {
+                    contractExprObj = new ContractExpr({ sourceTree: ledgerTree });
+                }
+                contractExprObj.initView(node);
+                $('#contract_expr').modal('show');
+            },
+            visible: function (key, opt) {
+                const node = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                return node.children.length === 0;
+            }
+        };
+    }
 
     $.contextMenu(billsContextMenuOptions);
 
@@ -3257,6 +3275,195 @@ $(document).ready(function() {
             return result;
         }
      }
+    class ContractExpr {
+        constructor(setting) {
+            const self = this;
+            this.obj = $('#contract_expr');
+            this.exprObj = $('#ce-expr');
+            this.exprSpread = SpreadJsObj.createNewSpread($('#expr_spread')[0]);
+            this.exprSheet = this.exprSpread.getActiveSheet();
+            const exprSpreadSetting = {
+                cols: [
+                    { title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 180, formatter: '@', cellType: 'tree' },
+                    { title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 100, formatter: '@' },
+                    { title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@' },
+                    { title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 60, formatter: '@', cellType: 'unit' },
+                ],
+                emptyRows: 0,
+                headRows: 2,
+                headRowHeight: [25, 25],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: true,
+                getColor: function (sheet, data, row, col, defaultColor) {
+                    if (self.targetNode && data && data.id === self.targetNode.id) {
+                        return spreadColor.pay.expr_err;
+                    } if (self.invalidRows && self.invalidRows.indexOf(row + 1) >= 0) {
+                        return spreadColor.pay.yf_without;
+                    } else {
+                        return defaultColor;
+                    }
+                }
+            };
+            sjsSettingObj.setFxTreeStyle(exprSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
+            SpreadJsObj.initSheet(this.exprSheet, exprSpreadSetting);
+            this.exprTree = createNewPathTree('ledger', {
+                id: 'ledger_id',
+                pid: 'ledger_pid',
+                order: 'order',
+                level: 'level',
+                rootId: -1,
+                keys: ['id', 'tender_id', 'ledger_id'],
+            });
+            this.exprTree.loadDatas(setting.sourceTree.datas);
+            SpreadJsObj.loadSheetData(this.exprSheet, SpreadJsObj.DataType.Tree, this.exprTree);
+
+            // this.exprSpread.bind(spreadNS.Events.SelectionChanged, function(e, info) {
+            //     const select = SpreadJsObj.getSelectObject(info.sheet);
+            //     if (select && self.targetNode && select.id === self.targetNode.id) {
+            //         $('#select-row').html('');
+            //     } else {
+            //         $('#select-row').html(`(${select.code || ''}${select.b_code || ''})`);
+            //     }
+            // });
+
+            this.obj.bind('shown.bs.modal', function () {
+                self.exprSpread.refresh();
+            });
+            $('td[exprValue]').dblclick(function() {
+                const name = this.getAttribute('name');
+                let row;
+                if (name === 'tr') {
+                    row = self.targetRow;
+                } else {
+                    const node = SpreadJsObj.getSelectObject(self.exprSheet);
+                    if (node.id === self.targetNode.id) return;
+                    row = self.exprTree.nodes.indexOf(node) + 1;
+                }
+                if (self.invalidRows.indexOf(row) >= 0) return;
+                self.exprObj.val(self.exprObj.val() + `<<f${row}$${this.getAttribute('exprValue')}>>`);
+            });
+            $('#contract-expr-ok').click(function() {
+                const exprStr = $('#ce-expr').val().toLowerCase();
+                if ((!exprStr && !self.orgExpr) || (exprStr === self.orgExpr)) {
+                    self.obj.modal('hide');
+                    return;
+                }
+
+                const [valid, msg] = TreeExprCalc.checkExprValid(exprStr, self.invalidRows);
+                if (!valid) {
+                    toastr.error(msg);
+                    return;
+                }
+                const expr = TreeExprCalc.exprStr2Expr(exprStr);
+                if (expr.length > 500) {
+                    toastr.error('输入表达式过长');
+                    return;
+                }
+                // $('#ce-expr-org').val(expr + '    ' + TreeExprCalc.expr2ExprStr(expr));
+                postData('expr/save', { calc_module: 'stage', calc_tag: 'contract', calc_id: self.targetNode.id, expr }, function() {
+                    self.targetNode.contract_expr = expr;
+                    self.obj.modal('hide');
+                });
+            });
+            this.initLedgerSearch();
+            this.initShowLevel();
+            TreeExprCalc.init(this.exprTree);
+        }
+        search(keyword) {
+            this.searchResult = [];
+            this.searchCur = 0;
+            if (keyword) {
+                for (const [i, d] of this.exprSheet.zh_tree.nodes.entries()) {
+                    if (d.code.indexOf(keyword) >= 0 || d.name.indexOf(keyword) >= 0) this.searchResult.push(d);
+                }
+            }
+            $('#expr_search_count').html(`结果:${this.searchResult.length}`);
+            if (this.searchResult.length > 0) SpreadJsObj.locateTreeNode(this.exprSheet, this.searchResult[0].ledger_id);
+        }
+        searchPre() {
+            this.searchCur = this.searchCur - 1;
+            if (this.searchCur < 0) this.searchCur = this.searchResult.length - 1;
+            SpreadJsObj.locateTreeNode(this.exprSheet, this.searchResult[this.searchCur].ledger_id);
+        }
+        searchNext() {
+            this.searchCur = this.searchCur + 1;
+            if (this.searchCur >= this.searchResult.length) this.searchCur = 0;
+            SpreadJsObj.locateTreeNode(this.exprSheet, this.searchResult[this.searchCur].ledger_id);
+        }
+        initLedgerSearch() {
+            const self = this;
+            this.searchResult = [];
+            this.searchCur = 0;
+            $('#expr_search_count').html(`结果:${this.searchResult.length}`);
+            $('#expr_search_keyword').change(function () { self.search(this.value); });
+            $('#expr_search_pre').click(function (e) {
+                self.searchPre();
+                e.stopPropagation();
+            });
+            $('#expr_search_next').click(function (e) {
+                self.searchNext();
+                e.stopPropagation();
+            });
+        }
+        initShowLevel(){
+            // 显示层次
+            (function (select, sheet) {
+                $(select).click(function () {
+                    const tag = $(this).attr('tag');
+                    const tree = sheet.zh_tree;
+                    if (!tree) return;
+                    setTimeout(() => {
+                        showWaitingView();
+                        switch (tag) {
+                            case "1":
+                            case "2":
+                            case "3":
+                            case "4":
+                            case "5":
+                                tree.expandByLevel(parseInt(tag));
+                                SpreadJsObj.refreshTreeRowVisible(sheet);
+                                break;
+                            case "last":
+                                tree.expandByCustom(() => { return true; });
+                                SpreadJsObj.refreshTreeRowVisible(sheet);
+                                break;
+                            case "leafXmj":
+                                tree.expandToLeafXmj();
+                                SpreadJsObj.refreshTreeRowVisible(sheet);
+                                break;
+                        }
+                        closeWaitingView();
+                    }, 100);
+                });
+            })('a[name=ce-showLevel]', this.exprSheet);
+        }
+        async setTargetNode(node) {
+            const self = this;
+            this.targetNode = node;
+            const relaNode = this.exprTree.getItems(node.ledger_id);
+            this.invalidNode = this.exprTree.getAllParents(relaNode);
+            this.invalidRows = this.invalidNode.map(x => {
+                return self.exprTree.nodes.indexOf(x) + 1;
+            });
+            this.targetRow = this.exprTree.nodes.indexOf(relaNode) + 1;
+            if (node.contract_expr === undefined) {
+                node.contract_expr = await postDataAsync('expr/load', {calc_module: 'stage', calc_tag: 'contract', calc_id: node.id});
+            }
+            this.orgExpr = node.contract_expr;
+            $('#ce-expr').val(TreeExprCalc.expr2ExprStr(this.orgExpr));
+        }
+        initView(node) {
+            this.orgExpr = '';
+            this.setTargetNode(node);
+            $('#expr-hint').html(`${node.code || ''}${node.b_code || ''} ${node.name || ''}`);
+            $('#target-row').html(`(${node.code || ''}${node.b_code || ''})`);
+            SpreadJsObj.locateTreeNode(this.exprSheet, node.ledger_id);
+            SpreadJsObj.reloadRowBackColor(this.exprSheet, 0, this.exprSheet.getRowCount());
+            this.exprSpread.refresh();
+        }
+    }
 
     // $('#searchAccount').click(() => {
     //     const data = {

+ 34 - 0
app/public/js/path_tree.js

@@ -570,6 +570,40 @@ const createNewPathTree = function (type, setting) {
             return posterity;
         };
         /**
+         * 递归方式 查询node的已下载的全部后代 (兼容full_path不存在的情况)
+         * @param node
+         * @returns {*}
+         * @private
+         */
+        _recursiveGetLeafPosterity(node) {
+            let posterity = node.children.filter(x => { return !x.children && x.children.length === 0; });
+            for (const c of node.children) {
+                posterity = posterity.concat(this._recursiveGetLeafPosterity(c));
+            }
+            return posterity;
+        };
+        /**
+         * 查询node的已下载的全部后代
+         * @param {Object} node
+         * @returns {Array}
+         */
+        getLeafPosterity(node) {
+            const self = this;
+            let posterity;
+            if (node[self.setting.fullPath] !== '') {
+                const reg = new RegExp('^' + node[self.setting.fullPath] + '-');
+                posterity = this.datas.filter(function (x) {
+                    return reg.test(x[self.setting.fullPath]) && x.is_leaf;
+                });
+            } else {
+                posterity = this._recursiveGetLeafPosterity(node);
+            }
+            posterity.sort(function (x, y) {
+                return self.getNodeIndex(x) - self.getNodeIndex(y);
+            });
+            return posterity;
+        };
+        /**
          * 查询node是否是父节点的最后一个子节点
          * @param {Object} node
          * @returns {boolean}

+ 228 - 0
app/public/js/shares/tree_expr_calc.js

@@ -0,0 +1,228 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const TreeExprCalc = (function(){
+    const calcRela = {
+        SerialReg: new RegExp('<<f[0-9]+\\$[a-z]+>>', 'ig'),
+        SerialFirstReg: new RegExp('^<<f[0-9]+\\$[a-z]+>>', 'i'),
+        IdReg: new RegExp('<<[a-z0-9\-]+\\$[a-z]+>>', 'ig'),
+        OrderReg: new RegExp('f[0-9]+', 'ig'),
+        valueChar: '$',
+        calcCache: {},
+        exprInitFields: ['contract_tp', 'qc_tp', 'gather_tp'],
+        calcType: {zero: 0, direct: 1, cache: 2}
+    };
+    const checkExprValid = function(expr, invalidOrders = []) {
+        if (!expr) return [true, ''];
+
+        const param = [];
+        let num = '';
+        for (let i = 0, iLen = expr.length; i < iLen; i++) {
+            const subExpr = expr.substring(i, expr.length);
+            if (/^[\d\.%]+/.test(expr[i])) {
+                num = num + expr[i];
+            } else if (calcRela.SerialFirstReg.test(subExpr)) {
+                if (num !== '') {
+                    param.push({type: 'num', value: num});
+                    num = '';
+                }
+                const order = calcRela.SerialFirstReg.exec(subExpr);
+                param.push({type: 'order', value: order[0]});
+                i = i + order[0].length - 1;
+            } else if (expr[i] === '(') {
+                if (num !== '') {
+                    param.push({type: 'num', value: num});
+                    num = '';
+                }
+                param.push({type: 'left', value: '('});
+            } else if (expr[i] === ')') {
+                if (num !== '') {
+                    param.push({type: 'num', value: num});
+                    num = '';
+                }
+                param.push({type: 'right', value: ')'});
+            } else if (/^[\+\-*\/]/.test(expr[i])) {
+                if (num !== '') {
+                    param.push({type: 'num', value: num});
+                    num = '';
+                }
+                param.push({type: 'calc', value: expr[i]});
+            } else {
+                return [false, '输入的表达式含有非法字符: ' + expr[i]];
+            }
+        }
+        if (num !== '') {
+            param.push({type: 'num', value: num});
+            num = '';
+        }
+        if (param.length === 0) return [true, ''];
+        if (param.length > 1) {
+            if (param[0].value === '-' && param[1].type === 'num') {
+                param[1].value = '-' + param[1].value;
+                param.shift();
+            }
+        }
+
+        const iLen = param.length;
+        let iLeftCount = 0, iRightCount = 0;
+        for (const [i, p] of param.entries()) {
+            if (p.type === 'calc') {
+                if (i === 0 || i === iLen - 1)
+                    return [false, '输入的表达式非法:计算符号' + p.value + '前后应有数字或计算基数'];
+            }
+            if (p.type === 'num') {
+                num = p.value.replace('%', '');
+                if (p.value.length - num.length > 1)
+                    return [false, '输入的表达式非法:' + p.value + '不是一个有效的数字'];
+                num = _.toNumber(num);
+                if (num === undefined || num === null || _.isNaN(num))
+                    return [false, '输入的表达式非法:' + p.value + '不是一个有效的数字'];
+                if (i > 0) {
+                    if (param[i - 1].type !== 'calc' && param[i - 1].type !== 'left') {
+                        return [false, '输入的表达式非法:' + p.value + '前应有运算符'];
+                    } else if (param[i - 1].value === '/' && num === 0) {
+                        return [false, '输入的表达式非法:请勿除0'];
+                    }
+                }
+            }
+            if (p.type === 'order') {
+                const match = invalidOrders.find(x => { p.value.indexOf(`f${x.orderStr}$`) > 0; });
+                if (match) return [false, `输入的表达式非法:循环引用,请勿引用${match.orderStr}`];
+
+                if (i > 0) {
+                    if (param[i - 1].type !== 'calc' && param[i - 1].type !== 'left') {
+                        return [false, '输入的表达式非法:' +  p.value.replace(/</ig, '&lt;').replace(/>/ig, '&gt;') + '前应有运算符'];
+                    }
+                }
+            }
+            if (p.type === 'left') {
+                iLeftCount += 1;
+                if (i !== 0 && param[i-1].type !== 'calc')
+                    return [false, '输入的表达式非法:(前应有运算符'];
+            }
+            if (p.type === 'right') {
+                iRightCount += 1;
+                if (i !== iLen - 1 && param[i+1].type !== 'calc')
+                    return [false, '输入的表达式非法:)后应有运算符'];
+                if (iRightCount > iLeftCount)
+                    return [false, '输入的表达式非法:")"前无对应的"("'];
+            }
+        }
+        if (iLeftCount > iRightCount)
+            return [false, '输入的表达式非法:"("后无对应的")"'];
+        return [true, ''];
+    };
+    const init = function(tree, decimal) {
+        calcRela.tree = tree;
+        calcRela.decimal = decimal;
+    };
+    const getCalcField = function(value) {
+        switch(value) {
+            case 'tzje': return 'total_price';
+            case 'qyje': return 'deal_tp';
+            case 'tzsl': return 'quantity';
+            case 'qysl': return 'deal_qty';
+            case 'bqhtje': return 'contract_tp';
+        }
+    };
+    const getCalcNumZero = function (node, field) {
+        return node.calc_expr && calcRela.exprInitFields.indexOf(field) >= 0 ? 0 : node[field] || 0;
+    };
+    const getCalcNumDirect = function (node, field) {
+        return node[field] || 0;
+    };
+    const getCalcNumCache = function (node, field) {
+        if (node.calc_expr && calcRela.exprInitFields.indexOf(field) >= 0) {
+            return calcRela.calcCache[node.id] ? calcRela.calcCache[node.id][field] || 0 : 0;
+        }
+        return node[field] || 0;
+    };
+    const getIdParamValue = function(idParam) {
+        const [id, value] = idParam.substring(2, idParam.length - 2).split(calcRela.valueChar);
+        const node = calcRela.tree.nodes.find(x => { return x.id === id; });
+        if (!node) return 0;
+
+        const calcField = getCalcField(value);
+
+        if (!node.children || node.children.length === 0) {
+            return calcRela.getCalcNum(node, calcField);
+        } else {
+            const posterity = calcRela.tree.getLeafPosterity(node);
+            const calcMap = posterity.map(x => { return calcRela.getCalcNum(x, calcField) });
+            return ZhCalc.sum(calcMap);
+        }
+    };
+    const calcExpr = function(expr) {
+        if (!expr) return 0;
+        let formula = expr;
+        const idParam = expr.match(calcRela.IdReg);
+        if (idParam) {
+            for (const ip of idParam) {
+                formula = formula.replace(ip, getIdParamValue(ip));
+            }
+        }
+        return [formula, math.evaluate(formula)];
+    };
+    const addCache = function(expr) {
+        const cache = { id: expr.id };
+        if (expr.calcField === 'contract_qty') {
+            cache.contract_qty = ZhCalc.round(expr.value, calcRela.decimal.qty);
+            cache.contract_tp = ZhCalc.mul(cache.contract_tp, expr.unit_price, calcRela.decimal.tp);
+        } else if (expr.calcField === 'contract_tp') {
+            cache.contract_qty = 0;
+            cache.contract_tp = ZhCalc.round(expr.value, calcRela.decimal.tp);
+        }
+        calcRela.calcCache[expr.id] = cache;
+    };
+    const calcAllExpr = function(exprList) {
+        calcRela.calcCache = {};
+        for (const expr of exprList) {
+            [expr.formula, expr.value] = calcExpr(expr.expr);
+            addCache(expr);
+        }
+        calcRela.calcCache = {};
+    };
+    const expr2ExprStr = function(expr) {
+        if (!expr) return '';
+        let formula = expr;
+        const idParam = expr.match(calcRela.IdReg);
+        if (idParam) {
+            for (const ip of idParam) {
+                const [id, value] = ip.substring(2, ip.length - 2).split(calcRela.valueChar);
+                const order = calcRela.tree.nodes.findIndex(x => { return x.id === id });
+                const orderParam = `<<f${order + 1}$${value}>>`;
+                formula = formula.replace(ip, orderParam);
+            }
+        }
+        return formula;
+    };
+    const exprStr2Expr = function(exprStr) {
+        if (!exprStr) return '';
+        let formula = exprStr;
+        const orderParam = exprStr.match(calcRela.SerialReg);
+        if (orderParam) {
+            for (const op of orderParam) {
+                const orderStr = op.match(calcRela.OrderReg)[0];
+                const order = parseInt(orderStr.substring(1, op.length));
+                const node = calcRela.tree.nodes[order - 1];
+                const idParam = op.replace(orderStr, node.id);
+                formula = formula.replace(op, idParam);
+            }
+        }
+        return formula;
+    };
+    const setCalcType = function (calcType) {
+        if (calcType === calcRela.calcType.zero) calcRela.getCalcNum = getCalcNumZero;
+        if (calcType === calcRela.calcType.direct) calcRela.getCalcNum = getCalcNumDirect;
+        if (calcType === calcRela.calcType.cache) calcRela.getCalcNum = getCalcNumCache;
+    };
+
+    return { init, checkExprValid, calcExpr, calcAllExpr, expr2ExprStr, exprStr2Expr, setCalcType, calcType: calcRela.calcType}
+})();

+ 128 - 1
app/public/js/stage.js

@@ -1673,6 +1673,132 @@ $(document).ready(() => {
                     return !node || !!node.b_code;
                 }
             },
+            'remainByExpr': {
+                name: '计算本期合同计量',
+                callback: function(key, opt) {
+                    const sheet = spSpread.getActiveSheet();
+                    const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+
+                    const [formula, value] = TreeExprCalc.calcExpr(node.calc_expr);
+                    const updateData = {lid: node.id, contract_expr: formula};
+                    if (node.is_tp) {
+                        updateData.contract_tp = value;
+                    } else {
+                        updateData.contract_qty = value;
+                    }
+
+                    postData(window.location.pathname + '/update', {bills: { stage: [updateData] }}, function (result) {
+                        const nodes = stageTree.loadPostStageData(result);
+                        stageTreeSpreadObj.refreshTreeNodes(slSpread.getActiveSheet(), nodes);
+                        if (detail) {
+                            detail.loadStageLedgerUpdateData(result, nodes);
+                        } else {
+                            stageIm.loadUpdateLedgerData(result, nodes);
+                        }
+                        stageTreeSpreadObj.loadExprToInput(sheet);
+                    });
+                },
+                visible: function(key, opt) {
+                    return !readOnly;
+                },
+                disabled: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+                    return !node || !node.calc_expr;
+                }
+            },
+            'remainByExprAll': {
+                name: '计算本期合同计量(全部公式项-0)',
+                callback: function(key, opt) {
+                    TreeExprCalc.setCalcType(TreeExprCalc.calcType.zero);
+                    const sheet = spSpread.getActiveSheet();
+
+                    const calcExpr = stageTree.nodes.filter(x => {
+                        return !!x.calc_expr;
+                    }).map(x => { return { id: x.id, expr: x.calc_expr, unit_price: x.unit_price, calcField: x.is_tp ? 'contract_tp' : 'contract_qty' }});
+                    TreeExprCalc.calcAllExpr(calcExpr);
+                    const updateData = calcExpr.map(x => {
+                        const data = { lid: x.id, contract_expr: x.formula };
+                        data[x.calcField] = x.value;
+                        return data;
+                    });
+
+                    postData(window.location.pathname + '/update', {bills: { stage: updateData }}, function (result) {
+                        const nodes = stageTree.loadPostStageData(result);
+                        stageTreeSpreadObj.refreshTreeNodes(slSpread.getActiveSheet(), nodes);
+                        if (detail) {
+                            detail.loadStageLedgerUpdateData(result, nodes);
+                        } else {
+                            stageIm.loadUpdateLedgerData(result, nodes);
+                        }
+                        stageTreeSpreadObj.loadExprToInput(sheet);
+                    });
+                },
+                visible: function(key, opt) {
+                    return !readOnly;
+                },
+            },
+            'remainByExprAll2': {
+                name: '计算本期合同计量(全部公式项-直取)',
+                callback: function(key, opt) {
+                    TreeExprCalc.setCalcType(TreeExprCalc.calcType.direct);
+                    const sheet = spSpread.getActiveSheet();
+
+                    const calcExpr = stageTree.nodes.filter(x => {
+                        return !!x.calc_expr;
+                    }).map(x => { return { id: x.id, expr: x.calc_expr, unit_price: x.unit_price, calcField: x.is_tp ? 'contract_tp' : 'contract_qty' }});
+                    TreeExprCalc.calcAllExpr(calcExpr);
+                    const updateData = calcExpr.map(x => {
+                        const data = { lid: x.id, contract_expr: x.formula };
+                        data[x.calcField] = x.value;
+                        return data;
+                    });
+
+                    postData(window.location.pathname + '/update', {bills: { stage: updateData }}, function (result) {
+                        const nodes = stageTree.loadPostStageData(result);
+                        stageTreeSpreadObj.refreshTreeNodes(slSpread.getActiveSheet(), nodes);
+                        if (detail) {
+                            detail.loadStageLedgerUpdateData(result, nodes);
+                        } else {
+                            stageIm.loadUpdateLedgerData(result, nodes);
+                        }
+                        stageTreeSpreadObj.loadExprToInput(sheet);
+                    });
+                },
+                visible: function(key, opt) {
+                    return !readOnly;
+                },
+            },
+            'remainByExprAll3': {
+                name: '计算本期合同计量(全部公式项-缓存)',
+                callback: function(key, opt) {
+                    const sheet = spSpread.getActiveSheet();
+                    TreeExprCalc.setCalcType(TreeExprCalc.calcType.cache);
+
+                    const calcExpr = stageTree.nodes.filter(x => {
+                        return !!x.calc_expr;
+                    }).map(x => { return { id: x.id, expr: x.calc_expr, unit_price: x.unit_price, calcField: x.is_tp ? 'contract_tp' : 'contract_qty' }});
+                    TreeExprCalc.calcAllExpr(calcExpr);
+                    const updateData = calcExpr.map(x => {
+                        const data = { lid: x.id, contract_expr: x.formula };
+                        data[x.calcField] = x.value;
+                        return data;
+                    });
+
+                    postData(window.location.pathname + '/update', {bills: { stage: updateData }}, function (result) {
+                        const nodes = stageTree.loadPostStageData(result);
+                        stageTreeSpreadObj.refreshTreeNodes(slSpread.getActiveSheet(), nodes);
+                        if (detail) {
+                            detail.loadStageLedgerUpdateData(result, nodes);
+                        } else {
+                            stageIm.loadUpdateLedgerData(result, nodes);
+                        }
+                        stageTreeSpreadObj.loadExprToInput(sheet);
+                    });
+                },
+                visible: function(key, opt) {
+                    return !readOnly;
+                },
+            },
             zjjlSpr: '----',
             'locateZjjl': {
                 name: '定位至中间计量',
@@ -2263,7 +2389,7 @@ $(document).ready(() => {
     });
 
     // 加载计量单元数据 - 暂时统一加载,如有需要,切换成动态加载并缓存
-    postData(window.location.pathname + '/load', { filter: 'ledger;pos;detail;change;import_change;tag;' }, function (result) {
+    postData(window.location.pathname + '/load', { filter: 'ledger;pos;detail;change;import_change;tag' }, function (result) {
         // 加载树结构
         stageTree.loadDatas(result.ledgerData, result.locked);
         if (stage.assist) stageTree.loadFilter(stage.assist.ass_ledger_id);
@@ -2280,6 +2406,7 @@ $(document).ready(() => {
         SpreadJsObj.loadTopAndSelect(slSpread.getActiveSheet(), ckBillsSpread);
         stagePosSpreadObj.loadCurPosData();
         SpreadJsObj.resetTopAndSelect(spSpread.getActiveSheet());
+        TreeExprCalc.init(stageTree, tenderInfo.decimal);
         // 加载中间计量
         stageIm.init(stage, imType, tenderInfo.decimal, stage.assist ? stage.assist.ass_ledger_id : '');
         stageIm.loadData(result.ledgerData, result.posData, result.detailData, result.changeData, result.import_change, result.detailAtt);

+ 3 - 0
app/router.js

@@ -200,6 +200,9 @@ module.exports = app => {
     app.post('/tender/:id/ctrl-price/update', sessionAuth, tenderCheck, 'ctrlPriceController.update');
     app.post('/tender/:id/ctrl-price/upload-excel/:ueType', sessionAuth, tenderCheck, 'ctrlPriceController.uploadExcel');
 
+    app.post('/tender/:id/expr/save', sessionAuth, tenderCheck, 'tenderController.saveExpr');
+    app.post('/tender/:id/expr/load', sessionAuth, tenderCheck, 'tenderController.loadExpr');
+
 
     // 预付款
     app.get('/tender/:id/advance/:type', sessionAuth, tenderCheck, 'advanceController.index');

+ 46 - 0
app/service/expr.js

@@ -0,0 +1,46 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2018/5/8
+ * @version
+ */
+
+const CalcModule = {
+    stage: 'stage', // 期
+};
+const CalcTag = {
+    contract: 'contract', //期-合同计量
+};
+
+module.exports = app => {
+    class Expr extends app.BaseService {
+
+        /**
+         * 构造函数
+         * @param ctx
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'expr';
+        }
+
+        async loadExpr(tid, data) {
+            const expr = await this.getDataByCondition({ tid, calc_module: data.calc_module, calc_tag: data.calc_tag, calc_id: data.calc_id });
+            return expr ? expr.expr : '';
+        }
+
+        async saveExpr(tid, data) {
+            const expr = await this.getDataByCondition({ tid, calc_module: data.calc_module, calc_tag: data.calc_tag, calc_id: data.calc_id });
+            if (expr) {
+                await this.defaultUpdate({id: expr.id, expr: data.expr, expr_bak: expr.expr });
+            } else {
+                const result = await this.db.insert(this.tableName, { tid, calc_module: data.calc_module, calc_tag: data.calc_tag, calc_id: data.calc_id, expr: data.expr });
+            }
+        }
+    }
+
+    return Expr;
+};

+ 1 - 0
app/view/ledger/explode.ejs

@@ -330,6 +330,7 @@
 <script src="/public/js/moment/moment.min.js"></script>
 <script type="text/javascript">
     const readOnly = <%- ctx.tender.ledgerReadOnly %>;
+    const contractExpr = <%- ctx.session.sessionProject.page_show.openContractExpr %>;
     const tender = JSON.parse('<%- JSON.stringify(tender) %>');
     const tenderInfo = JSON.parse(unescape('<%- escape(JSON.stringify(tenderInfo)) %>'));
     const thousandth = <%- ctx.tender.info.display.thousandth %>;

+ 81 - 0
app/view/ledger/explode_modal.ejs

@@ -388,6 +388,87 @@
         </div>
     </div>
 </div>
+<% if (ctx.session.sessionProject.page_show.openContractExpr) { %>
+<div class="modal fade" id="contract_expr" data-backdrop="static">
+    <div class="modal-dialog modal-xl" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">预设本期合同计量公式:<span id="expr-hint"></span></h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                </div>
+                <div class="form-group">
+                    <div class="row">
+                        <div class="col-7">
+                            <div class="d-inline-flex">
+                                <!--显示层级-->
+                                <div class="d-inline-block">
+                                    <div class="dropdown">
+                                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                            <i class="fa fa-list-ol"></i> 显示层级
+                                        </button>
+                                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                                            <a class="dropdown-item" name="ce-showLevel" tag="1" href="javascript:void(0);">第一层</a>
+                                            <a class="dropdown-item" name="ce-showLevel" tag="2" href="javascript:void(0);">第二层</a>
+                                            <a class="dropdown-item" name="ce-showLevel" tag="3" href="javascript:void(0);">第三层</a>
+                                            <a class="dropdown-item" name="ce-showLevel" tag="4" href="javascript:void(0);">第四层</a>
+                                            <a class="dropdown-item" name="ce-showLevel" tag="5" href="javascript:void(0);">第五层</a>
+                                            <a class="dropdown-item" name="ce-showLevel" tag="last" href="javascript:void(0);">最底层</a>
+                                            <a class="dropdown-item" name="ce-showLevel" tag="leafXmj" href="javascript:void(0);">只显示项目节</a>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="mx-2 mb-2">
+                                    <div class="input-group input-group-sm">
+                                        <input type="text" class="form-control" placeholder="输入编号/名称查找" id="expr_search_keyword">
+                                        <div class="input-group-append">
+                                            <span class="input-group-text" id="expr_search_count">结果:20</span>
+                                        </div>
+                                        <div class="input-group-append">
+                                            <button class="btn btn-outline-secondary" type="button" title="上一个" id="expr_search_pre"><i class="fa fa-angle-double-left"></i></button>
+                                            <button class="btn btn-outline-secondary" type="button" title="下一个" id="expr_search_next"><i class="fa fa-angle-double-right"></i></button>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div id="expr_spread" style="height: 420px"></div>
+                        </div>
+                        <div class="col">
+                            <div class="mb-4 mt-2">
+                                <h6>可用计算值</h6>
+                                <div style="width: 200px;" >
+                                    <table class="table table-bordered mb-1">
+                                        <tr><th class="text-left">引用行<span id="select-row"></span></th></tr>
+                                        <tr><td class="text-center" name="sr" exprValue="tzje">台账金额</td></tr>
+                                        <tr><td class="text-center" name="sr" exprValue="qyje">签约金额</td></tr>
+                                        <tr><td class="text-center" name="sr" exprValue="bqhtje">本期合同金额</td></tr>
+                                        <tr><th class="text-left">设置行<span id="target-row"></span></th></tr>
+                                        <tr><td class="text-center" name="tr" exprValue="tzje">台账</td></tr>
+                                        <tr><td class="text-center" name="tr" exprValue="qyje">签约</td></tr>
+                                    </table>
+                                </div>
+                                <div class="text-danger mb-3">点击添加对应行值到公式框</div>
+                            </div>
+                            <div>
+                               <h6>本期合同计量公式</h6>
+                                <textarea class="form-control form-control-sm" rows="7" id="ce-expr"></textarea>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="from-group" style="display: none;">
+                    <textarea class="form-control form-control-sm" rows="1" id="ce-expr-org"></textarea>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-primary btn-sm" id="contract-expr-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
 
 <% if (ctx.session.sessionUser.accountId === ctx.tender.data.user_id && (ctx.tender.data.ledger_status === auditConst.status.uncheck || ctx.tender.data.ledger_status === auditConst.status.checkNo)) { %>
     <script>

+ 2 - 0
config/web.js

@@ -200,6 +200,7 @@ const JsFiles = {
                     '/public/js/zh_calc.js',
                     '/public/js/zip_oss.js',
                     '/public/js/path_tree.js',
+                    '/public/js/shares/tree_expr_calc.js',
                     '/public/js/ledger_tree_col.js',
                     '/public/js/std_lib.js',
                     '/public/js/ledger_check.js',
@@ -377,6 +378,7 @@ const JsFiles = {
                     '/public/js/shares/new_tag.js',
                     '/public/js/zh_calc.js',
                     '/public/js/path_tree.js',
+                    '/public/js/shares/tree_expr_calc.js',
                     '/public/js/zip_oss.js',
                     '/public/js/shares/ali_oss.js',
                     '/public/js/stage_im.js',