Browse Source

合同支付,计算相关

MaiXinRong 8 months ago
parent
commit
5a74952d2e

+ 8 - 8
app/base/base_tree_service.js

@@ -101,7 +101,7 @@ class TreeService extends Service {
     }
 
     async getDataByKidAndCount(mid, kid, count) {
-        if ((mid <= 0) || (kid <= 0)) return [];
+        if (!mid || (kid <= 0)) return [];
         const select = await this.getDataByKid(mid, kid);
         if (!select) throw '数据错误';
 
@@ -273,7 +273,7 @@ class TreeService extends Service {
      * @return {Promise<*>}
      */
     async getChildrenByParentId(mid, pid) {
-        if (mid <= 0 || !pid) return undefined;
+        if (!mid || !pid) return undefined;
         const pids = pid instanceof Array ? pid : [pid];
 
         this.initSqlBuilder();
@@ -299,7 +299,7 @@ class TreeService extends Service {
      * @return {Promise<void>}
      */
     async getPosterityByParentId(mid, pid) {
-        if (mid <= 0 || !pid) return undefined;
+        if (!mid || !pid) return undefined;
 
         const node = await this.getDataByKid(mid, pid);
 
@@ -551,7 +551,7 @@ class TreeService extends Service {
      * @return {Array} - 被删除的数据
      */
     async deleteNode(mid, kid) {
-        if ((mid <= 0) || (kid <= 0)) return [];
+        if (!mid || (kid <= 0)) return [];
         const select = await this.getDataByKid(mid, kid);
         if (!select) throw '删除节点数据错误';
         const parent = await this.getDataByKid(mid, select[this.setting.pid]);
@@ -597,7 +597,7 @@ class TreeService extends Service {
     }
 
     async deleteNodes(mid, kid, count) {
-        if ((mid <= 0) || (kid <= 0) || (count <= 0)) return [];
+        if (!mid || (kid <= 0) || (count <= 0)) return [];
         const selects = await this.getDataByKidAndCount(mid, kid, count);
         const first = selects[0];
         const parent = await this.getDataByKid(mid, first[this.setting.pid]);
@@ -658,7 +658,7 @@ class TreeService extends Service {
      */
     async upMoveNode(mid, kid, count) {
         if (!count) count = 1;
-        if (!mid || (mid <= 0) || !kid || (kid <= 0)) return null;
+        if (!mid || !kid || (kid <= 0)) return null;
         const selects = await this.getDataByKidAndCount(mid, kid, count);
         if (selects.length !== count) throw '上移节点数据错误';
         const first = selects[0];
@@ -699,7 +699,7 @@ class TreeService extends Service {
      */
     async downMoveNode(mid, kid, count) {
         if (!count) count = 1;
-        if (!mid || (mid <= 0) || !kid || (kid <= 0)) return null;
+        if (!mid || !kid || (kid <= 0)) return null;
         const selects = await this.getDataByKidAndCount(mid, kid, count);
         if (selects.length !== count) {
             throw '下移节点数据错误';
@@ -1018,7 +1018,7 @@ class TreeService extends Service {
      * @return {Array} - 提交后的数据
      */
     async updateInfos(mid, datas) {
-        if (mid <= 0) throw '数据错误';
+        if (!mid) throw '数据错误';
 
         if (Array.isArray(datas)) {
             const updateDatas = [];

+ 19 - 18
app/controller/pay_controller.js

@@ -69,6 +69,9 @@ module.exports = app => {
 
                 const newPhase = await ctx.service.phasePay.add(ctx.tender.id, stages, date, memo);
                 if (!newPhase) throw '新增期失败';
+                newPhase.curTimes = 1;
+                newPhase.curOrder = 0;
+                await ctx.service.phasePayDetail.calculateSave(newPhase);
 
                 ctx.redirect('/tender/' + ctx.tender.id + '/pay' + newPhase.phase_order);
             } catch (err) {
@@ -88,6 +91,7 @@ module.exports = app => {
 
         async detail(ctx) {
             try {
+                await this.ctx.service.phasePayDetail.calculateSave(ctx.phasePay);
                 const pays = await this.ctx.service.phasePayDetail.getDetailData(ctx.phasePay);
                 const calcBase = this.ctx.service.phasePay.getPhasePayCalcBase(ctx.phasePay, ctx.tender.info);
                 const renderData = {
@@ -103,22 +107,6 @@ module.exports = app => {
             }
         }
 
-        async _detailBase(phasePay, type, data) {
-            if (isNaN(data.id) || data.id <= 0) throw '数据错误';
-            if (type !== 'add') {
-                if (isNaN(data.count) || data.count <= 0) data.count = 1;
-            }
-            switch (type) {
-                case 'add':
-                    return await this.ctx.service.phasePayDetail.addDetailNode(phasePay, data.id, data.count);
-                case 'delete':
-                    return await this.ctx.service.phasePayDetail.delete(phasePay.id, data.id, data.count);
-                case 'up-move':
-                    return await this.ctx.service.phasePayDetail.upMoveNode(phasePay.id, data.id, data.count);
-                case 'down-move':
-                    return await this.ctx.service.phasePayDetail.downMoveNode(phasePay.id, data.id, data.count);
-            }
-        }
         async detailUpdate(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
@@ -127,14 +115,27 @@ module.exports = app => {
 
                 switch (data.postType) {
                     case 'add':
+                        responseData.data = await this.ctx.service.phasePayDetail.addDetailNode(ctx.phasePay, data.postData.id, data.postData.count || 1);
+                        break;
                     case 'delete':
+                        await this.ctx.service.phasePayDetail.delete(ctx.phasePay.id, data.postData.id, data.postData.count || 1);
+                        await this.ctx.service.phasePayDetail.calculateSave(ctx.phasePay);
+                        responseData.data.reload = await this.ctx.service.phasePayDetail.getDetailData(ctx.phasePay);
                     case 'up-move':
+                        responseData.data = await this.ctx.service.phasePayDetail.upMoveNode(ctx.phasePay.id, data.postData.id, data.postData.count || 1);
+                        break;
                     case 'down-move':
-                        responseData.data = await this._billsBase(ctx.detail, data.postType, data.postData);
+                        responseData.data = await this.ctx.service.phasePayDetail.downMoveNode(ctx.phasePay.id, data.postData.id, data.postData.count || 1);
                         break;
                     case 'update':
-                        responseData.data = await this.ctx.service.phasePayDetail.updateCalc(ctx.detail, data.postData);
+                        await this.ctx.service.phasePayDetail.updateCalc(ctx.phasePay, data.postData);
+                        responseData.data.reload = await this.ctx.service.phasePayDetail.calculateSave(ctx.phasePay);
                         break;
+                    case 'calc':
+                        await this.ctx.service.phasePayDetail.calculateSave(ctx.phasePay);
+                        responseData.data.reload = await this.ctx.service.phasePayDetail.getDetailData(ctx.phasePay);
+                    case 'refreshBase':
+                        await this.ctx.service.phasePay.refreshCalcBase()
                     default:
                         throw '未知操作';
                 }

+ 1 - 0
app/lib/analysis_excel.js

@@ -578,6 +578,7 @@ class AnalysisExcelTree {
             node.total_price = this.ctx.helper.round(aeUtils.toNumber(row[this.colsDef.total_price]), this.decimal.tp);
             node.drawing_code = this.ctx.helper.replaceReturn(row[this.colsDef.drawing_code]);
             node.memo = this.ctx.helper.replaceReturn(row[this.colsDef.memo]);
+            this.ctx.helper.checkDgnQtyPrecision(data);
             return this.cacheTree.addXmjNode(node);
         } catch (error) {
             if (error.stack) {

+ 1 - 0
app/lib/ledger.js

@@ -1285,6 +1285,7 @@ class reviseTree extends billsTree {
 }
 
 module.exports = {
+    baseTree,
     billsTree,
     pos,
     filterTree,

+ 377 - 42
app/public/js/phase_pay_detail.js

@@ -27,52 +27,387 @@ $(document).ready(() => {
                     }
                 }
             }
+        },
+        check: {
+            isFixed: function(data) {
+                return data.is_fixed;
+            },
+            isStarted: function (data) {
+                return data.pre_used;
+            },
+            isYf: function(data) {
+                return data.pay_type === 'bqyf';
+            },
+            isSf: function(data) {
+                return data.pay_type === 'bqsf';
+            }
         }
     };
-    class PayObj {
-        constructor () {
-            this.spread = SpreadJsObj.createNewSpread($('#pay-spread')[0]);
-            this.sheet = this.spread.getActiveSheet();
-            this.spreadSetting = {
-                cols: [
-                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 230, formatter: '@', cellType: 'tree', getTip: payUtils.tips.name},
-                    {title: '本期金额(F)', colSpan: '1', rowSpan: '1', field: 'tp', hAlign: 2, width: 100, type: 'Number'},
-                    {title: '截止上期金额',  colSpan: '1', rowSpan: '1', field: 'pre_tp', hAlign: 2, width: 100, readOnly: true, type: 'Number'},
-                    {title: '截止本期金额',  colSpan: '1', rowSpan: '1', field: 'end_tp', hAlign: 2, width: 100, readOnly: true, type: 'Number'},
-                    {title: '起扣金额',  colSpan: '1', rowSpan: '1', field: 'start_tp', hAlign: 2, width: 100, type: 'Number'},
-                    {title: '付(扣)款限额',  colSpan: '1', rowSpan: '1', field: 'range_tp', hAlign: 2, width: 100, cellType: 'tip', type: 'Number', getTip: payUtils.tips.range_tp},
-                    {title: '汇总',  colSpan: '1', rowSpan: '1', field: 'is_yf', hAlign: 2, width: 60, readOnly: true, type: 'checkbox'},
-                    {title: '附件', colSpan: '1', rowSpan: '1', field: 'attachment', hAlign: 0, width: 60, readOnly: true, cellType: 'imageBtn', normalImg: '#rela-file-icon', hoverImg: '#rela-file-hover', getValue: 'getValue.attachment'},
-                    {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 120, formatter: '@', cellType: 'autoTip'},
-                ],
-                emptyRows: 0,
-                headRows: 1,
-                headRowHeight: [32],
-                headColWidth: [30],
-                defaultRowHeight: 21,
-                headerFont: '12px 微软雅黑',
-                font: '12px 微软雅黑',
-                readOnly: readOnly,
-                localCache: {
-                    key: 'phase-pay',
-                    colWidth: true,
-                },
-                pos: SpreadJsObj.getObjPos($('#pay-spread')[0]),
-            };
-            sjsSettingObj.setFxTreeStyle(this.spreadSetting, sjsSettingObj.FxTreeStyle.phasePay);
-            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
-            this.payTree = createNewPathTree('base', {
-                id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
-                level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
-                rootId: -1,
-            });
+    const payCalc = (function (b) {
+        class PayCalc {
+            constructor (bases) {
+                this.percentReg = /((\d+)|((\d+)(\.\d+)))%/g;
+                this.bases = bases;
+                this.bases.sort(function (a, b) {
+                    return a.sort - b.sort;
+                });
+                for (const b of this.bases) {
+                    b.reg = new RegExp(b.code, 'igm');
+                }
+                this.orderReg = /f\d+/ig;
+                this.nodeReg = /<<[a-z0-9\-]+>>/ig;
+            }
+            trans2OrderExpr(expr, payTree) {
+                const nodeParam = expr.match(this.nodeReg);
+                if (nodeParam) {
+                    for (const op of nodeParam) {
+                        const id = op.substring(2, op.length - 2);
+                        const payNode = payTree.nodes.find(x => { return x.uuid === id; });
+                        expr = expr.replace(op, payNode ? `f${payTree.getNodeIndex(payNode) + 1}` || '' : 0);
+                    }
+                }
+                return expr;
+            }
+            trans2NodeExpr(expr, payTree) {
+                const orderParam = expr.match(this.orderReg);
+                if (orderParam) {
+                    for (const op of orderParam) {
+                        const order = parseInt(op.substring(1, op.length));
+                        const payNode = payTree.nodes[order - 1];
+                        expr = expr.replace(op, payNode ? `<<${payNode.uuid}>>` || '' : 0);
+                    }
+                }
+                return expr;
+            }
+            checkSfExpr() {
+
+            }
+            checkExpr() {
+
+            }
+            checkRangeExpr() {
+
+            }
+            checkStartExpr() {
+
+            }
+            getExprInfo(field, converse = false) {
+                const exprField = [
+                    {qty: 'tp', expr: 'expr'},
+                    {qty: 'start_tp', expr: 'start_expr'},
+                    {qty: 'range_qty', expr: 'range_expr'},
+                ];
+                if (converse) return _.find(exprField, { expr: field });
+                return _.find(exprField, {qty: field});
+            }
+
+            getLeafOrder(data, parentOrder) {
+                if (!data) return [];
+                const defaultResult = data.order ? [`f${data.order}`] : [];
+                if (!data.expr) return defaultResult;
+                const orderParam = data.expr.match(this.orderReg);
+                if (!orderParam || orderParam.length === 0) return defaultResult;
+
+                const result = [], payData = paySheet.zh_data || [];
+                for (const op of orderParam) {
+                    const order = op.substring(1, op.length);
+                    if (data.order === parseInt(order) || op === parentOrder) {
+                        result.push(op);
+                    } else {
+                        const subOrderParam = this.getLeafOrder(payData[parseInt(order) -1], data.order ? `f${data.order}` : parentOrder);
+                        result.push(...subOrderParam);
+                    }
+                }
+                return result;
+            }
+
+            checkCircularExpr(expr, selfOrder) {
+                const leafOrder = this.getLeafOrder({expr}, `f${selfOrder}`);
+
+                if (leafOrder.indexOf(`f${selfOrder}`) >= 0 || leafOrder.indexOf(`F${selfOrder}`) >= 0) return true;
+                return false;
+            }
+
+            calculateExpr(expr) {
+                let formula = expr;
+                for (const b of this.bases) {
+                    formula = formula.replace(b.reg, b.value);
+                }
+                const percent = formula.match(this.percentReg);
+                if (percent) {
+                    for (const p of percent) {
+                        const v = math.evaluate(p.replace(new RegExp('%', 'gm'), '/100'));
+                        formula = formula.replace(p, v);
+                    }
+                }
+                try {
+                    const value = math.evaluate(formula);
+                    return value;
+                } catch(err) {
+                    return 0;
+                }
+            }
         }
-        loadDatas(datas) {
-            this.payTree.loadDatas(datas);
-            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.payTree);
+
+        return new PayCalc(b);
+    })(calcBase);
+    const payObj = (function() {
+        const spread = SpreadJsObj.createNewSpread($('#pay-spread')[0]);
+        const sheet = spread.getActiveSheet();
+        const spreadSetting = {
+            cols: [
+                {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 230, formatter: '@', cellType: 'tree', getTip: payUtils.tips.name},
+                {title: '本期金额(F)', colSpan: '1', rowSpan: '1', field: 'tp', hAlign: 2, width: 100, type: 'Number'},
+                {title: '截止上期金额',  colSpan: '1', rowSpan: '1', field: 'pre_tp', hAlign: 2, width: 100, readOnly: true, type: 'Number'},
+                {title: '截止本期金额',  colSpan: '1', rowSpan: '1', field: 'end_tp', hAlign: 2, width: 100, readOnly: true, type: 'Number'},
+                {title: '起扣金额',  colSpan: '1', rowSpan: '1', field: 'start_tp', hAlign: 2, width: 100, type: 'Number'},
+                {title: '付(扣)款限额',  colSpan: '1', rowSpan: '1', field: 'range_tp', hAlign: 2, width: 100, cellType: 'tip', type: 'Number', getTip: payUtils.tips.range_tp},
+                {title: '汇总',  colSpan: '1', rowSpan: '1', field: 'is_yf', hAlign: 2, width: 60, readOnly: true, type: 'checkbox'},
+                {title: '附件', colSpan: '1', rowSpan: '1', field: 'attachment', hAlign: 0, width: 60, readOnly: true, cellType: 'imageBtn', normalImg: '#rela-file-icon', hoverImg: '#rela-file-hover', getValue: 'getValue.attachment'},
+                {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 120, formatter: '@', cellType: 'autoTip'},
+            ],
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            headColWidth: [30],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            readOnly: readOnly,
+            localCache: {
+                key: 'phase-pay',
+                colWidth: true,
+            },
+            pos: SpreadJsObj.getObjPos($('#pay-spread')[0]),
+        };
+        sjsSettingObj.setFxTreeStyle(spreadSetting, sjsSettingObj.FxTreeStyle.phasePay);
+        SpreadJsObj.initSheet(sheet, spreadSetting);
+        const payTree = createNewPathTree('base', {
+            id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
+            level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
+            rootId: -1,
+        });
+        const payEvent = {
+            refreshActn: function() {
+                const setObjEnable = function (obj, enable) {
+                    if (enable) {
+                        obj.removeClass('disabled');
+                    } else {
+                        obj.addClass('disabled');
+                    }
+                };
+                const select = SpreadJsObj.getSelectObject(sheet);
+                const preNode = payTree.getPreSiblingNode(select);
+                setObjEnable($('a[name=base-opr][type=add]'), !readOnly && !payUtils.check.isSf(select) && !payUtils.check.isYf(select));
+                const delValid = !payUtils.check.isFixed(select) && !payUtils.check.isStarted(select);
+                setObjEnable($('a[name=base-opr][type=del]'), !readOnly && select && delValid);
+                setObjEnable($('a[name=base-opr][type=up-move]'), !readOnly && select && !payUtils.check.isFixed(select) && preNode);
+                setObjEnable($('a[name=base-opr][type=down-move]'), !readOnly && select && !payUtils.check.isFixed(select) && !this.payTree.isLastSibling(select));
+            },
+            loadExprToInput: function() {
+                const sel = sheet.getSelections()[0];
+                const col = sheet.zh_setting.cols[sel.col];
+                const data = SpreadJsObj.getSelectObject(this.sheet);
+                if (data) {
+                    if (col.field === 'tp') {
+                        $('#expr').val(data.expr).attr('field', 'expr').attr('org', data.expr)
+                            .attr('readOnly', readOnly|| payCol.readOnly.tp(data));
+                    } else if (col.field === 'stage_tp') {
+                        $('#expr').val(data.start_expr).attr('field', 'start_expr').attr('org', data.start_expr)
+                            .attr('readOnly', readOnly|| payCol.readOnly.sprice(data) || payBase.isYF(data));
+                    } else if (col.field === 'rprice') {
+                        $('#expr').val(data.range_expr).attr('field', 'range_expr').attr('org', data.range_expr)
+                            .attr('readOnly', readOnly|| payCol.readOnly.rprice(data) || payBase.isYF(data));
+                    } else {
+                        $('#expr').val('').attr('readOnly', true);
+                    }
+                    $('#expr').attr('data-row', sel.row);
+                } else {
+                    $('#expr').val('').attr('readOnly', true);
+                    $('#expr').removeAttr('data-row');
+                }
+            },
+            refreshTree: function (data) {
+                SpreadJsObj.massOperationSheet(sheet, function () {
+                    const tree = sheet.zh_tree;
+                    // 处理删除
+                    if (data.delete) {
+                        data.delete.sort(function (a, b) {
+                            return b.deleteIndex - a.deleteIndex;
+                        });
+                        for (const d of data.delete) {
+                            sheet.deleteRows(d.deleteIndex, 1);
+                        }
+                    }
+                    // 处理新增
+                    if (data.create) {
+                        const newNodes = data.create;
+                        if (newNodes) {
+                            newNodes.sort(function (a, b) {
+                                return a.index - b.index;
+                            });
+                            for (const node of newNodes) {
+                                sheet.addRows(node.index, 1);
+                                SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(node), 1);
+                            }
+                        }
+                    }
+                    // 处理更新
+                    if (data.update) {
+                        const rows = [];
+                        for (const u of data.update) {
+                            rows.push(tree.nodes.indexOf(u));
+                            billsTag.refreshBillsTagView(u);
+                        }
+                        SpreadJsObj.reLoadRowsData(sheet, rows);
+                    }
+                });
+            },
+            editStarting: function(e, info) {
+                const col = info.sheet.zh_setting.cols[info.col];
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                switch (col.field) {
+                    case 'name':
+                        info.cancel = payUtils.check.isFixed(select);
+                        break;
+                    case 'tp':
+                    case 'is_gather':
+                        info.cancel = select.children && select.children.length > 0;
+                        break;
+                    case 'start_tp':
+                    case 'range_tp':
+                        info.cancel = (select.children && select.children.length > 0) || payUtils.check.isYf(select);
+                        break;
+                }
+
+                if (col.field === 'tp') {
+                    if (select.expr && select.expr !== '') {
+                        info.sheet.getCell(info.row, info.col).text(payCalc.trans2OrderExpr(select.expr, payTree));
+                    }
+                } else if (col.field === 'start_tp') {
+                    if (select.start_expr && select.start_expr !== '') {
+                        info.sheet.getCell(info.row, info.col).text(select.start_expr);
+                    }
+                } else if (col.field === 'range_tp') {
+                    if (select.range_expr && select.range_expr !== '') {
+                        info.sheet.getCell(info.row, info.col).text(select.range_expr);
+                    }
+                }
+            },
+            editEnded: function(e, info) {
+                if (!info.sheet.zh_setting) return;
+
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+                if (col.field === 'is_gather') return;
+                // 未改变值则不提交
+                const validText = info.editingText ? info.editingText.replace('\n', '') : null;
+                let orgValue;
+                if (col.field === 'tp') {
+                    orgValue = select.expr ? payCalc.trans2OrderExpr(select.expr, payTree) : select.tp;
+                } else if (col.field === 'start_tp') {
+                    orgValue = select.start_expr ? select.start_expr : select.start_tp;
+                } else if (col.field === 'range_tp') {
+                    orgValue = select.range_expr ? select.range_expr : select.range_tp;
+                } else {
+                    orgValue = select[col.field];
+                }
+                if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                const data = { postType: 'update', postData: {} };
+                switch(col.field) {
+                    case 'tp':
+                        const [tpValid, tpMsg] = payUtils.isSF(select)
+                            ? payCalc.checkSfExpr(validText, data.updateData, select.order)
+                            : payCalc.checkExpr(validText, data.updateData, select.order);
+                        if (!tpValid) {
+                            toastr.warning(tpMsg);
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                        break;
+                    case 'start_tp':
+                        const [sValid, sMsg] = payCalc.checkStartExpr(select, validText, data.updateData);
+                        if (!sValid) {
+                            toastr.warning(sMsg);
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                        break;
+                    case 'range_tp':
+                        const [rValid, rMsg] = payCalc.checkRangeExpr(select, validText, data.updateData);
+                        if (!rValid) {
+                            toastr.warning(rMsg);
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                        break;
+                    default:
+                        if (col.type === 'Number') {
+                            data.postData[col.field] = _.toNumber(validText) || 0;
+                        } else {
+                            data.postData[col.field] = validText || '';
+                        }
+                        break;
+                }
+                postData('update', data, function (result) {
+                    payEvent.reloadPays(result.reload);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            },
+            selectionChanged: function(e, info) {
+                if (info.newSelections) {
+                    if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                        payEvent.refreshActn();
+                    }
+                }
+                payEvent.loadExprToInput();
+            },
+            baseOpr: function(type) {
+                const node = SpreadJsObj.getSelectObject(sheet);
+
+                if (type === 'del') {
+                    postData('update', {postType: 'del', postData: { id: node.tree_id }}, function(result) {
+                        payEvent.reloadPays(result.reload);
+                    });
+                } else {
+                    postData('update', { postType: type, postData: { id: node.tree_id }}, function (result) {
+                        const refreshData = payTree.loadPostData(result);
+                        payEvent.refreshTree(sheet, refreshData);
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            if (['up-move', 'down-move'].indexOf(type) > -1) {
+                                sheet.setSelection(payTree.getNodeIndex(node), sel.col, sel.rowCount, sel.colCount);
+                                SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, payTree.getNodeIndex(node)]);
+                            } else if (type === 'add') {
+                                sheet.setSelection(payTree.getNodeIndex(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                                SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, payTree.getNodeIndex(refreshData.create[0])]);
+                            }
+                        }
+                        self.refreshOperationValid(sheet);
+                    });
+                }
+            },
+            reloadPays: function(datas){
+                payTree.loadDatas(datas);
+                SpreadJsObj.loadSheetData(sheet, SpreadJsObj.DataType.Tree, payTree);
+                payEvent.refreshActn();
+            }
+        };
+        spread.bind(spreadNS.Events.SelectionChanged, payEvent.selectionChanged);
+        if (!readOnly) {
+            spread.bind(spreadNS.Events.EditStarting, payEvent.editStarting);
+            spread.bind(spreadNS.Events.EditEnded, payEvent.editEnded);
+            $('a[name="base-opr"]').click(function () {
+                payEvent.baseOpr(this.getAttribute('type'));
+            });
         }
-    }
-    const payObj = new PayObj();
+
+        return { spread, sheet, payTree, loadDatas: payEvent.reloadPays }
+    })();
     payObj.loadDatas(details);
 
     // todo 加载审批列表

+ 1 - 0
app/router.js

@@ -397,6 +397,7 @@ module.exports = app => {
     app.post('/tender/:id/pay/del', sessionAuth, tenderCheck, 'payController.del');
     app.post('/tender/:id/pay/save', sessionAuth, tenderCheck, 'payController.save');
     app.get('/tender/:id/pay/:order/detail', sessionAuth, tenderCheck, phasePayCheck, 'payController.detail');
+    app.post('/tender/:id/pay/:order/update', sessionAuth, tenderCheck, phasePayCheck, 'payController.detailUpdate');
 
     // 变更概况
     app.get('/tender/:id/measure/stage/:order/change', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.change');

+ 1 - 0
app/service/ledger.js

@@ -712,6 +712,7 @@ module.exports = app => {
                 id: 'ledger_id', pid: 'ledger_pid', order: 'order', full_path: 'full_path', level: 'level', rootId: -1,
                 calcFields: [ 'total_price' ],
                 calc(node, helper, decimal) {
+                    helper.checkDgnQtyPrecision(node);
                     const precision = helper.findPrecision(tender.info.precision, node.unit);
                     node.quantity = helper.round(node.quantity, precision.value);
                     node.unit_price = helper.round(node.unit_price, decimal.up);

+ 35 - 25
app/service/phase_pay.js

@@ -15,8 +15,8 @@ const calcBase = [
     {name: '签约合同价(不含暂列金)', code: 'htjszl', sort: 1},
     {name: '签约开工预付款', code: 'kgyfk', sort: 2},
     {name: '签约材料预付款', code: 'clyfk', sort: 2},
-    {name: '本期完成计量', code: 'bqwc', limit: true, sort: 10},
-    {name: '本期合同计量', code: 'bqht', limit: true, sort: 10},
+    {name: '本期完成计量', code: 'bqwc', limit: true, sort: 10, checkStart: 'gather_tp'},
+    {name: '本期合同计量', code: 'bqht', limit: true, sort: 10, checkStart: 'contract_tp'},
     {name: '本期变更计量', code: 'bqbg', limit: true, sort: 10},
     {name: '本期清单完成计量', code: 'bqqdwc', limit: true, sort: 10},
     {name: '本期清单合同计量', code: 'bqqdht', limit: true, sort: 10},
@@ -259,31 +259,42 @@ module.exports = app => {
             }
         }
 
-        async refreshCalcBase(id) {
-            const curPay = await this.getPhasePay(id);
-            if (!curPay) throw '合同支付不存在, 请刷新页面重试';
-
+        async refreshCalcBase(phasePay) {
             const calcBase = await this.getCalcBase(relaStage);
-            await this.defaultUpdate({
-                id, update_user_id: this.ctx.session.sessionUser.accountId,
-                calc_base: JSON.stringify(calcBase),
-                rela_stage: JSON.stringify(relaStage.map(s => { return {stage_id: s.id, stage_order: s.order}; })),
-                calc_base_time: new Date()
-            });
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.tableName, {
+                    id: phasePay.id, update_user_id: this.ctx.session.sessionUser.accountId,
+                    calc_base: JSON.stringify(calcBase),
+                    calc_base_time: new Date()
+                });
+                phasePay.calc_base = calcBase;
+                await this.ctx.service.phasePayDetail.calculateSave(phasePay, conn);
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
         }
-
-        async resetRelaStageId(id, relaStage) {
-            const curPay = await this.getPhasePay(id);
-            if (!curPay) throw '合同支付不存在, 请刷新页面重试';
-
-            if (await this._checkRelaStageConflict(relaStage, curPay)) throw '选择的计量期,已被调用,请刷新页面后选择计量期新增合同支付';
+        async resetRelaStageId(phasePay, relaStage) {
+            if (await this._checkRelaStageConflict(relaStage, phasePay)) throw '选择的计量期,已被调用,请刷新页面后选择计量期新增合同支付';
             const calcBase = await this.getCalcBase(relaStage);
-            await this.defaultUpdate({
-                id, update_user_id: this.ctx.session.sessionUser.accountId,
-                calc_base: JSON.stringify(calcBase),
-                rela_stage: JSON.stringify(relaStage.map(s => { return {stage_id: s.id, stage_order: s.order}; })),
-                calc_base_time: new Date()
-            });
+            const rela_stage = relaStage.map(s => { return {stage_id: s.id, stage_order: s.order}; });
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.tableName, {
+                    id: phasePay.id, update_user_id: this.ctx.session.sessionUser.accountId,
+                    calc_base: JSON.stringify(calcBase),
+                    rela_stage: JSON.stringify(rela_stage),
+                    calc_base_time: new Date()
+                });
+                phasePay.calc_base = calcBase;
+                phasePay.rela_stage = relaStage;
+                await this.ctx.service.phasePayDetail.calculateSave(phasePay, conn);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
         }
 
         async loadUser(phasePay) {
@@ -332,7 +343,6 @@ module.exports = app => {
                 phasePay.canCheck = phasePay.readOnly && stage.curAuditorIds.indexOf(accountId) > 0;
             }
         }
-
     }
 
     return PhasePay;

+ 320 - 10
app/service/phase_pay_detail.js

@@ -9,19 +9,300 @@
  */
 
 const payType = {
-    bqsf: 'bqsf', bqyf: 'bqyf', gcjl: 'gcjl', qtfk: 'qtfk', qtkk: 'qtkk', bqwc: 'bqwc'
+    bqsf: 'bqsf', bqyf: 'bqyf', gcjl: 'gcjl', qtfk: 'qtfk', qtkk: 'qtkk'
 };
 const defaultPays = [
-    { tree_id: 1, tree_pid: -1, tree_level: 1, tree_order: 1, tree_is_leaf: 1, tree_full_path: '1', pay_type: 'bqsf', is_minus: 0, is_fixed: 1, name: '本期应付' },
-    { tree_id: 2, tree_pid: -1, tree_level: 1, tree_order: 2, tree_is_leaf: 1, tree_full_path: '1', pay_type: 'bqyf', is_minus: 0, is_fixed: 1, name: '本期应付' },
-    { tree_id: 3, tree_pid: -1, tree_level: 1, tree_order: 3, tree_is_leaf: 0, tree_full_path: '1', pay_type: 'gcjl', is_minus: 0, is_fixed: 1, name: '工程计量款' },
-    { tree_id: 4, tree_pid: -1, tree_level: 1, tree_order: 4, tree_is_leaf: 0, tree_full_path: '1', pay_type: 'qtfk', is_minus: 0, is_fixed: 1, name: '其他付款项' },
-    { tree_id: 5, tree_pid: -1, tree_level: 1, tree_order: 5, tree_is_leaf: 0, tree_full_path: '1', pay_type: 'qtkk', is_minus: 1, is_fixed: 1, name: '其他扣款项' },
-    { tree_id: 6, tree_pid: 3, tree_level: 2, tree_order: 1, tree_is_leaf: 1, tree_full_path: '3-6', pay_type: 'bqwc', is_minus: 0, is_fixed: 0, name: '本期完成计量', expr: 'bqwc' },
+    { tree_id: 1, tree_pid: -1, tree_level: 1, tree_order: 1, tree_is_leaf: 1, tree_full_path: '1', pay_type: 'bqyf', is_minus: 0, is_fixed: 1, name: '本期应付' },
+    { tree_id: 2, tree_pid: -1, tree_level: 1, tree_order: 2, tree_is_leaf: 1, tree_full_path: '2', pay_type: 'bqsf', is_minus: 0, is_fixed: 1, name: '本期实付' },
+    { tree_id: 3, tree_pid: -1, tree_level: 1, tree_order: 3, tree_is_leaf: 0, tree_full_path: '3', pay_type: 'gcjl', is_minus: 0, is_fixed: 1, name: '工程计量款' },
+    { tree_id: 4, tree_pid: -1, tree_level: 1, tree_order: 4, tree_is_leaf: 0, tree_full_path: '4', pay_type: 'qtfk', is_minus: 0, is_fixed: 1, name: '其他付款项' },
+    { tree_id: 5, tree_pid: -1, tree_level: 1, tree_order: 5, tree_is_leaf: 0, tree_full_path: '5', pay_type: 'qtkk', is_minus: 1, is_fixed: 1, name: '其他扣款项' },
+    { tree_id: 6, tree_pid: 3, tree_level: 2, tree_order: 1, tree_is_leaf: 1, tree_full_path: '3-6', pay_type: '', is_minus: 0, is_fixed: 0, name: '本期完成计量', expr: 'bqwc' },
     { tree_id: 7, tree_pid: 4, tree_level: 2, tree_order: 1, tree_is_leaf: 1, tree_full_path: '4-7', pay_type: '', is_minus: 0, is_fixed: 0, name: '新增付款项' },
     { tree_id: 8, tree_pid: 5, tree_level: 2, tree_order: 1, tree_is_leaf: 1, tree_full_path: '5-8', pay_type: '', is_minus: 1, is_fixed: 0, name: '新增扣款项' },
 ];
 const TreeService = require('../base/base_tree_service');
+const Ledger = require('../lib/ledger');
+const deallineType = {
+    phaseCount: 'phaseCount',
+    stageCount: 'stageCount',
+    gather: 'gather',
+    contract: 'contract',
+};
+const math = require('mathjs');
+math.config({
+    number: 'BigNumber',
+});
+
+class PayCalculator {
+    constructor (ctx, phasePay) {
+        this.ctx = ctx;
+        this.phasePay = phasePay;
+        this.percentReg = /((\d+)|((\d+)(\.\d+)))%/g;
+        this.tenderInfo = ctx.tender.info;
+        this.decimal = this.tenderInfo.decimal.pay ? this.tenderInfo.decimal.payTp : this.tenderInfo.decimal.tp;
+        this.orderReg = /f\d+/ig;
+        this.nodeReg = /<<[a-z0-9\-]+>>/ig;
+        /* 以下变量在调用calculate方法后获得
+        this.add;
+        this.pre;
+        this.cur;
+        this.yf;
+         */
+    }
+
+    /**
+     * 获取 计算基数
+     * @returns {Promise<void>}
+     */
+    getCalcBase () {
+        if (this.bases) return;
+
+        const bases = this.ctx.service.phasePay.getPhasePayCalcBase(this.phasePay, this.tenderInfo);
+        this.bases = bases.sort(function (a, b) {
+            return a.sort - b.sort;
+        });
+        for (const b of this.bases) {
+            b.reg = new RegExp(b.code, 'igm');
+        }
+    }
+    getCalcAdd() {
+        if (this.addBase) return;
+
+        const calc_base = this.phasePay.calc_base;
+        this.addBase = {
+            contract_tp: this.ctx.helper.add(calc_base.contract_tp, calc_base.pre_contract_tp),
+            qc_tp: this.ctx.helper.add(calc_base.qc_tp, calc_base.pre_qc_tp),
+            gather_tp: this.ctx.helper.add(calc_base.gather_tp, calc_base.pre_gather_tp),
+        };
+    }
+
+    _calculateTpExpr(pay, pays) {
+        let formula = pay.expr;
+        const nodeParam = pay.expr.match(this.nodeReg);
+        if (nodeParam) {
+            for (const op of nodeParam) {
+                const id = op.substring(2, op.length - 2);
+                const payNode = pays.find(x => { return x.uuid === id; });
+                formula = formula.replace(op, payNode ? payNode.tp || 0 : 0);
+            }
+        }
+        for (const b of this.bases) {
+            const value = b.checkStart && (!pay.pre_used && pay.start_tp)
+                ? this.ctx.helper.sub(this.addBase[pay.checkStart], pay.start_tp)
+                : b.value;
+            formula = formula.replace(b.reg, value);
+        }
+        const percent = formula.match(this.percentReg);
+        if (percent) {
+            for (const p of percent) {
+                const v = math.eval(p.replace(new RegExp('%', 'gm'), '/100'));
+                formula = formula.replace(p, v);
+            }
+        }
+        try {
+            // 使用mathjs计算 math.eval('17259401.95*0.9') = 15533461.754999999,正确应为 15533461.755
+            // const value = math.eval(formula);
+            // 使用逆波兰法四则运算,可防止出现误差,但是只支持四则运算,不支持科学运算
+            // const value = this.ctx.helper.calcExprStrRpn(formula);
+            // 使用mathjs的大数运算,可支持所有
+            const value = parseFloat(math.eval(formula));
+            return Number.isFinite(value) ? value : 0;
+        } catch(err) {
+            return 0;
+        }
+    }
+    _calculateExpr(expr) {
+        let formula = expr;
+        for (const b of this.bases) {
+            formula = formula.replace(b.reg, b.value ? b.value : 0);
+        }
+        const percent = formula.match(this.percentReg);
+        if (percent) {
+            for (const p of percent) {
+                const v = math.eval(p.replace(new RegExp('%', 'gm'), '/100'));
+                formula = formula.replace(p, v);
+            }
+        }
+        try {
+            // 使用mathjs计算 math.eval('17259401.95*0.9') = 15533461.754999999,正确应为 15533461.755
+            // const value = math.eval(formula);
+            // 使用逆波兰法四则运算,可防止出现误差,但是只支持四则运算,不支持科学运算
+            // const value = this.ctx.helper.calcExprStrRpn(formula);
+            // 使用mathjs的大数运算,可支持所有
+            const value = parseFloat(math.eval(formula));
+            return Number.isFinite(value) ? value : 0;
+        } catch(err) {
+            return 0;
+        }
+    }
+    /**
+     * 计算起扣金额、付(扣)款限额
+     *
+     * @param {Array} pays - (标段)合同支付数据
+     */
+    calculateStartRangePrice (payTree) {
+        for (const p of payTree.nodes) {
+            // 上一期已计量的合同支付项,不予计算起扣金额、扣款限额
+            if (p.pre_used) continue;
+
+            if (p.start_expr && p.start_expr !== '') {
+                p.start_tp = this.ctx.helper.round(this._calculateExpr(p.start_expr), this.decimal);
+            } else if (p.start_tp && !p.start_expr) {
+                p.start_tp = this.ctx.helper.round(p.start_tp, this.decimal);
+            }
+            if (p.range_expr && p.range_expr !== '') {
+                p.range_price = this.ctx.helper.round(this._calculateExpr(p.range_expr), this.decimal);
+            } else if (p.range_price && !p.range_expr) {
+                p.range_price = this.ctx.helper.round(p.range_price, this.decimal);
+            }
+        }
+    }
+    /**
+     * 检查是否到达 计提期限
+     * @param pay
+     */
+    _checkDeadline(pay) {
+        switch (pay.dl_type) {
+            case deallineType.phaseCount:
+                return this.phasePay.phase_order >= pay.dl_value;
+            case deallineType.stageCount:
+                const maxStageOrder = this.ctx.helper._.max(this.phasePay.rela_stage.map(x => { return x.stage_order; }));
+                return maxStageOrder >= pay.dl_value;
+            case deallineType.gather:
+            case deallineType.contract:
+                const deallineTp = this.add[pay.dl_type + '_tp'];
+                return deallineTp >= pay.dl_value;
+            default :
+                return false;
+        }
+    }
+
+    getLeafOrder(data, pays, parentId) {
+        if (!data || !data.expr) return [];
+        const nodeParam = data.expr.match(this.nodeReg);
+        if (!nodeParam || nodeParam.length === 0) return [];
+
+        const result = [...nodeParam];
+        for (const op of nodeParam) {
+            const id = op.substring(2, op.length - 4);
+            if (id === data.uuid || id === parentId) {
+                result.push(op);
+            } else {
+                const subPay = pays.find(x => { return x.uuid === id; });
+                const sub = this.getLeafOrder(subPay, pays, data.uuid);
+                if (sub.length > 0) {
+                    result.push(...sub);
+                } else {
+                    result.push(op);
+                }
+            }
+        }
+        return this.ctx.helper._.uniq(result);
+    }
+    sortPaysByCalc(payTree) {
+        const calcArr = [];
+        for (const pay of payTree.nodes) {
+            if (pay.payType === payType.bqyf) {
+                pay.calcSort = 3;
+            } else if (pay.payType === payType.bqsf) {
+                pay.calcSort = 4;
+            } else if (pay.children && pay.children.length > 0) {
+                pay.calcSort = 2;
+            } else {
+                pay.calcSort = 1;
+                pay.calcLeaf = this.getLeafOrder(pay, payTree.nodes);
+            }
+            calcArr.push(pay);
+        }
+        calcArr.sort((x, y) => {
+            const calcSort = x.calcSort - y.calcSort;
+            if (calcSort) return calcSort;
+            if (x.calcSort === 2) return -(x.tree_level - y.tree_level);
+            return x.calcLeaf.length - y.calcLeaf.length;
+        });
+        return calcArr;
+    }
+
+    _calculateYf(yf, pays) {
+        yf.tp = 0;
+        for (const p of pays) {
+            if (p.payType || !p.is_gather || !p.tree_is_leaf) continue;
+            yf.tp = !p.is_minus ? this.ctx.helper.add(yf.tp, p.tp) : this.ctx.helper.sub(yf.tp, p.tp);
+        }
+        const bqyf = this.bases.find(function (x) {return x.code === 'bqyf'});
+        if (bqyf) bqyf.value = yf.tp;
+    }
+    _calculateSf(sf, pays) {
+        if (sf.expr) {
+            sf.tp = this.ctx.helper.round(this._calculateExpr(sf, pays), this.decimal);
+        } else {
+            const yf = pays.find(x => { return x.pay_type === payType.bqyf; });
+            sf.tp = yf.tp;
+        }
+        sf.tp = sf.range_tp ? Math.min(this.ctx.helper.sub(sf.range_tp, sf.pre_tp), sf.tp) : sf.tp;
+    }
+    _calculateGather(pay) {
+        pay.tp = 0;
+        for (const c of pay.children) {
+            if (c.children && c.children.length > 0) this._calculateGather(c);
+            if (!c.is_gather) continue;
+            pay.tp = this.ctx.helper.add(pay.tp, c.tp);
+        }
+    }
+    _calculateCommon(pay, pays) {
+        // 暂停计量|未达起扣金额时,不计算
+        if (pay.is_pause || (pay.start_tp && this.addBase.gather_tp < pay.start_tp)) {
+            pay.tp = 0;
+            return;
+        }
+        if (this._checkDeadline(pay)) {
+           pay.tp = this.ctx.helper.sub(pay.range_tp, pay.pre_tp);
+        } else if (pay.expr) {
+            pay.tp = this.ctx.helper.round(this._calculateTpExpr(pay, pays), this.decimal);
+        } else {
+            pay.tp = this.ctx.helper.round(pay.tp || 0, this.decimal);
+        }
+        pay.tp = pay.range_tp ? Math.min(this.ctx.helper.sub(pay.range_tp, pay.pre_tp), pay.tp) : pay.tp;
+    }
+    /**
+     * 计算本期、截止本期金额
+     * @param {Array} pays - (标段&期)合同支付数据
+     */
+    calculate(pays) {
+        for (const p of pays) {
+            switch (p.pay_type) {
+                case payType.bqyf:
+                    this._calculateYf(p, pays); break;
+                case payType.bqsf:
+                    this._calculateSf(p, pays); break;
+                case payType.gcjl:
+                case payType.qtfk:
+                case payType.qtkk:
+                    this._calculateGather(p); break;
+                default:
+                    this._calculateCommon(p, pays); break;
+            }
+            p.end_tp = this.ctx.helper.add(p.pre_tp, p.tp);
+        }
+    }
+
+    calculateAll(payTree) {
+        payTree.nodes.forEach(x => {
+            x.org_tp = x.tp || 0;
+            x.org_start_tp = x.start_tp || 0;
+            x.org_range_tp = x.range_tp || 0;
+        });
+        this.getCalcBase();
+        this.getCalcAdd();
+        this.calculateStartRangePrice(payTree);
+        const calcPays = this.sortPaysByCalc(payTree);
+        this.calculate(calcPays);
+        payTree.nodes.forEach(p => {
+            p.calcUpdate = !this.ctx.helper.numEqual(p.org_tp, p.tp) || !this.ctx.helper.numEqual(p.org_start_tp, p.start_tp) || !this.ctx.helper.numEqual(p.org_range_tp, p.range_tp);
+        });
+    }
+}
 
 class PhasePayDetail extends TreeService {
 
@@ -100,6 +381,35 @@ class PhasePayDetail extends TreeService {
             return await this.getAllDataByCondition({ where: { phase_id: phasePay.id, audit_times: phasePay.audit_times, audit_sort: phasePay.audit_max_order } });
         }
     }
+    calculate(phasePay, details) {
+        const payTree = new Ledger.baseTree(this.ctx, {
+            id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
+            level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
+            rootId: -1, calcField: [],
+        });
+        payTree.loadDatas(details);
+        const payCalculator = new PayCalculator(this.ctx, phasePay);
+        payCalculator.calculateAll(payTree);
+        return payTree.getDefaultDatas();
+    }
+
+    async calculateSave(phasePay, transaction) {
+        const details = await this.getDetailData(phasePay);
+        if (details.length === 0) return false;
+
+        const calcResult = this.calculate(phasePay, details);
+        const updateData = calcResult.filter(x => { return x.calcUpdate; }).map(x => {
+            return { id: x.id, tp: x.tp, start_tp: x.start_tp, range_tp: x.range_tp };
+        });
+        if (updateData.length === 0) return false;
+
+        if (transaction) {
+            await transaction.updateRows(this.tableName, updateData);
+        } else {
+            await this.defaultUpdateRows(updateData);
+        }
+        return true;
+    }
 
     _getDefaultData(data, phasePay, parent) {
         data.uuid = this.uuid.v4();
@@ -116,7 +426,7 @@ class PhasePayDetail extends TreeService {
         if (select.payType === payType.bqsf || select.payType === payType.bqyf) throw '不可新增子项';
         if (select.tree_level >= 2) throw '不可新增子项';
 
-        const children = await this.getChildrenByParentId(phasePay.id, select[this.setting.id]);
+        const children = await this.getChildrenByParentId(phasePay.id, select[this.setting.kid]);
         const maxId = await this._getMaxLid(phasePay.id);
         const insertData = [];
         for (let i = 0; i < count; i++) {
@@ -144,7 +454,7 @@ class PhasePayDetail extends TreeService {
 
         // 查询应返回的结果
         const resultData = {};
-        resultData.create = await this.getNextsData(phasePay.id, data.tree_id, children.length);
+        resultData.create = await this.getNextsData(phasePay.id, data[0].tree_id, children.length);
         if (children.length === 0) resultData.update = await this.getDataByKid(phasePay.id, select.tree_id);
         return resultData;
     }
@@ -193,7 +503,7 @@ class PhasePayDetail extends TreeService {
     async addDetailNode(phasePay, targetId, count = 1) {
         if (!phasePay) return null;
 
-        const select = targetId ? await this.getDataByKid(detail.id, targetId) : null;
+        const select = targetId ? await this.getDataByKid(phasePay.id, targetId) : null;
         if (targetId && !select) throw '新增节点数据错误';
 
         if (select[this.setting.level] === 1) {

+ 7 - 2
app/view/phase_pay/detail.ejs

@@ -13,12 +13,16 @@
                 <div class="d-inline-block ">
                     <div class="input-group input-group-sm ml-2 mt-1">
                         <div class="input-group-prepend">
-                            <span class="input-group-text" id="expr">表达式</span>
+                            <span class="input-group-text">表达式</span>
                         </div>
-                        <input type="text" class="form-control m-0" style="width: 270px" value="">
+                        <input type="text" class="form-control m-0" style="width: 270px" value="" id="pay-expr">
                     </div>
                 </div>
             </div>
+            <div class="ml-auto">
+                <button class="btn btn-sm btn-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="计算全部"><i class="fa fa-play"></i></button>
+                <button class="btn btn-sm btn-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="刷新基数"><i class="fa fa-repeat"></i></button>
+            </div>
         </div>
     </div>
     <div class="content-wrap">
@@ -58,4 +62,5 @@
 <script>
     const readOnly = <%- ctx.phasePay.readOnly %>;
     const details = JSON.parse('<%- JSON.stringify(pays) %>');
+    const calcBase = JSON.parse('<%- JSON.stringify(calcBase) %>');
 </script>