Sfoglia il codice sorgente

台账修订,部位明细,新增、删除、编辑、复制粘贴、清除

MaiXinRong 5 anni fa
parent
commit
99aa9fff43

+ 153 - 0
app/base/base_bills_service.js

@@ -11,6 +11,8 @@
 const TreeService = require('./base_tree_service');
 // sql拼装器
 const rootId = -1;
+const calcFields = ['unit_price', 'sgfh_qty', 'sgfh_tp', 'sjcl_qty', 'sjcl_tp', 'qtcl_qty', 'qtcl_tp', 'deal_qty', 'deal_tp', 'dgn_qty1', 'dgn_qty2'];
+const readOnlyFields = ['id', 'tender_id', 'ledger_id', 'ledger_pid', 'order', 'level', 'full_path', 'is_leaf'];
 
 class BaseBillsSerivce extends TreeService {
 
@@ -266,6 +268,157 @@ class BaseBillsSerivce extends TreeService {
         return { create: createData, update: updateData };
     }
 
+    /**
+     * 过滤data中update方式不可提交的字段
+     * @param {Number} id - 主键key
+     * @param {Object} data
+     * @return {Object<{id: *}>}
+     * @private
+     */
+    _filterUpdateInvalidField(id, data) {
+        const result = {
+            id,
+        };
+        for (const prop in data) {
+            if (readOnlyFields.indexOf(prop) === -1) {
+                result[prop] = data[prop];
+            }
+        }
+        return result;
+    }
+
+    /**
+     * newData中,以orgData为基准,过滤掉orgData中未定义或值相等的部分
+     * @param {Object} orgData
+     * @param {Object} newData
+     * @private
+     */
+    _filterChangedField(orgData, newData) {
+        const result = {};
+        let bChanged = false;
+        for (const prop in orgData) {
+            if (this._.isEmpty(newData[prop]) && newData[prop] !== orgData[prop]) {
+                result[prop] = newData[prop];
+                bChanged = true;
+            }
+        }
+        return bChanged ? result : undefined;
+    }
+
+    /**
+     * 检查data中是否含有计算字段
+     * @param {Object} data
+     * @return {boolean}
+     * @private
+     */
+    _checkCalcField(data) {
+        for (const prop in data) {
+            if (calcFields.indexOf(prop) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 提交数据 - 响应计算(增量方式计算)
+     * @param {Number} tenderId
+     * @param {Object} data
+     * @return {Promise<*>}
+     */
+    async updateCalc(tenderId, data) {
+        // 简单验证数据
+        if (tenderId <= 0 || !this.ctx.tender) {
+            throw '标段不存在';
+        }
+        const info = this.ctx.tender.info;
+        if (!data) {
+            throw '提交数据错误';
+        }
+        const datas = data instanceof Array ? data : [data];
+        const ids = [];
+        for (const row of datas) {
+            if (tenderId !== row.tender_id) {
+                throw '提交数据错误';
+            }
+            ids.push(row.id);
+        }
+
+        this.transaction = await this.db.beginTransaction();
+        try {
+            for (const row of datas) {
+                const updateNode = await this.getDataById(row.id);
+                if (!updateNode || tenderId !== updateNode.tender_id || row.ledger_id !== updateNode.ledger_id) {
+                    throw '提交数据错误';
+                }
+                let updateData;
+                if (row.unit) {
+                    if (row.sgfh_qty === undefined) { row.sgfh_qty = updateNode.sgfh_qty; }
+                    if (row.sjcl_qty === undefined) { row.sjcl_qty = updateNode.sjcl_qty; }
+                    if (row.qtcl_qty === undefined) { row.qtcl_qty = updateNode.qtcl_qty; }
+                    if (row.deal_qty === undefined) { row.deal_qty = updateNode.deal_qty; }
+                }
+                if (row.b_code) {
+                    row.dgn_qty1 = null;
+                    row.dgn_qty2 = null;
+                }
+                if (this._checkCalcField(row)) {
+                    let calcData = JSON.parse(JSON.stringify(row));
+                    const precision = this.ctx.helper.findPrecision(info.precision, row.unit ? row.unit : updateNode.unit);
+                    // 数量保留小数位数
+                    this.ctx.helper.checkFieldPrecision(calcData, qtyFields, precision.value);
+                    // 单位保留小数位数
+                    this.ctx.helper.checkFieldPrecision(calcData, upFields, info.decimal.up);
+                    // 未提交单价则读取数据库单价
+                    if (row.unit_price === undefined) calcData.unit_price = updateNode.unit_price;
+                    // 计算
+                    if (row.sgfh_qty !== undefined || row.sjcl_qty !== undefined || row.qtcl_qty !== undefined ||
+                        row.deal_qty !== undefined || row.unit_price) {
+                        if (row.sgfh_qty === undefined) calcData.sgfh_qty = updateNode.sgfh_qty;
+                        if (row.sjcl_qty === undefined) calcData.sjcl_qty = updateNode.sjcl_qty;
+                        if (row.qtcl_qty === undefined) calcData.qtcl_qty = updateNode.qtcl_qty;
+                        if (row.deal_qty === undefined) calcData.deal_qty = updateNode.deal_qty;
+                        calcData.quantity = this.ctx.helper.sum([calcData.sgfh_qty, calcData.sjcl_qty, calcData.qtcl_qty]);
+                        calcData.sgfh_tp = this.ctx.helper.mul(calcData.sgfh_qty, calcData.unit_price, info.decimal.tp);
+                        calcData.sjcl_tp = this.ctx.helper.mul(calcData.sjcl_qty, calcData.unit_price, info.decimal.tp);
+                        calcData.qtcl_tp = this.ctx.helper.mul(calcData.qtcl_qty, calcData.unit_price, info.decimal.tp);
+                        calcData.total_price = this.ctx.helper.mul(calcData.quantity, calcData.unit_price, info.decimal.tp);
+                        calcData.deal_tp = this.ctx.helper.mul(calcData.deal_qty, calcData.unit_price, info.decimal.tp);
+                    } else if (row.sgfh_tp !== undefined || row.sjcl_tp !== undefined || row.qtcl_tp !== undefined || row.deal_tp !== undefined) {
+                        calcData.sgfh_qty = null;
+                        calcData.sjcl_qty = null;
+                        calcData.qtcl_qty = null;
+                        calcData.quantity = null;
+                        calcData.deal_qty = null;
+                        calcData.sgfh_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.sgfh_tp, info.decimal.tp) : updateNode.sgfh_tp;
+                        calcData.sjcl_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.sjcl_tp, info.decimal.tp) : updateNode.sjcl_tp;
+                        calcData.qtcl_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.qtcl_tp, info.decimal.tp) : updateNode.qtcl_tp;
+                        calcData.total_price = this.ctx.helper.sum([calcData.sgfh_tp, calcData.sjcl_tp, calcData.qtcl_tp]);
+                        calcData.deal_tp = (row.deal_tp !== undefined) ? this.ctx.helper.round(calcData.row.deal_tp, info.decimal.tp) : updateNode.deal_tp;
+                    } else if (row.unit_price !== undefined) {
+                        calcData.sgfh_tp = this.ctx.helper.mul(calcData.sgfh_qty, calcData.unit_price, info.decimal.tp);
+                        calcData.sjcl_tp = this.ctx.helper.mul(calcData.sjcl_qty, calcData.unit_price, info.decimal.tp);
+                        calcData.qtcl_tp = this.ctx.helper.mul(calcData.qtcl_qty, calcData.unit_price, info.decimal.tp);
+                        calcData.total_price = this.ctx.helper.mul(calcData.quantity, calcData.unit_price, info.decimal.tp);
+                        calcData.deal_tp = this.ctx.helper.mul(calcData.deal_qty, calcData.unit_price, info.decimal.tp);
+                    }
+                    updateData = this._filterUpdateInvalidField(updateNode.id, calcData);
+                } else {
+                    updateData = this._filterUpdateInvalidField(updateNode.id, row);
+                }
+                await this.transaction.update(this.tableName, updateData);
+            }
+            await this.transaction.commit();
+            this.transaction = null;
+        } catch (err) {
+            await this.transaction.rollback();
+            this.transaction = null;
+            throw err;
+        }
+
+        return { update: await this.getDataById(ids) };
+    }
+
 }
 
 module.exports = BaseBillsSerivce;

+ 55 - 7
app/controller/revise_controller.js

@@ -19,7 +19,6 @@ const measureType = require('../const/tender').measureType;
 const spreadConst = require('../const/spread');
 const fs = require('fs');
 const LzString = require('lz-string');
-const accountGroup = require('../const/account_group').group;
 
 module.exports = app => {
 
@@ -243,6 +242,31 @@ module.exports = app => {
                 renderData.posSpread.readOnly = true;
             }
             renderData.readOnly = readOnly;
+            if (!readOnly) {
+                renderData.usedInfo = {};
+                const lastStage = await ctx.service.stage.getLastestStage(ctx.tender.id, true);
+                if (lastStage.status === audit.stage.status.checked) {
+                    const usedPreBills = await ctx.service.stageBillsFinal.getUsedBills(ctx.tender.id, lastStage.order);
+                    for (const b of renderData.reviseBills) {
+                        b.used = usedPreBills.indexOf(b.id) >= 0;
+                    }
+                    const usedPrePos = await ctx.service.stagePosFinal.getUsedPos(ctx.tender.id, lastStage.order);
+                    for (const p of renderData.revisePos) {
+                        p.used = usedPrePos.indexOf(p.id) >= 0;
+                    }
+                } else {
+                    const usedPreBills = lastStage.order > 1 ? await ctx.service.stageBillsFinal.getUsedBills(ctx.tender.id, lastStage.order - 1) : [];
+                    const usedCurBills = await ctx.service.stageBills.getStageUsedBills(ctx.tender.id, lastStage.id);
+                    for (const b of renderData.reviseBills) {
+                        b.used = usedPreBills.indexOf(b.id) >= 0 || usedCurBills.indexOf(b.id) >= 0;
+                    }
+                    const usedPrePos = lastStage.order > 1 ? await ctx.service.stagePosFinal.getUsedPos(ctx.tender.id, lastStage.order - 1) : [];
+                    const usedCurPos = await ctx.service.stageBills.getStageUsedPos(ctx.tender.id, lastStage.id);
+                    for (const p of renderData.revisePos) {
+                        p.used = usedPrePos.indexOf(b.id) >= 0 || usedCurPos.indexOf(p.id) >= 0;
+                    }
+                }
+            }
             renderData.history = false;
             renderData.historyRevise = [];
             await this.layout('revise/info.ejs', renderData, 'revise/info_modal.ejs');
@@ -284,14 +308,14 @@ module.exports = app => {
          * @param ctx
          * @returns {Promise<void>}
          */
-        async _billsBase(ctx, data) {
+        async _billsBase(ctx, 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 ctx.service.reviseBills.addNode(ctx.tender.id, ctx.revise.id, data.id);
+                    return await ctx.service.reviseBills.addReviseNode(ctx.tender.id, ctx.revise.id, data.id);
                 case 'delete':
                     return await ctx.service.reviseBills.delete(ctx.tender.id, data.id, data.count);
                 case 'up-move':
@@ -379,6 +403,22 @@ module.exports = app => {
                 throw '数据错误';
             }
         }
+        async _updatePos(ctx, data) {
+            await this.checkMeasureType(measureType.tz.value);
+            if (!data.posPostType) throw '数据错误';
+            switch (data.posPostType) {
+                case 'add':
+                    return await ctx.service.revisePos.addPos(ctx.tender.id, ctx.revise.id, data.postData);
+                case 'update':
+                    return await ctx.service.revisePos.updatePos(ctx.tender.id, data.postData);
+                case 'delete':
+                    return await ctx.service.revisePos.deletePos(ctx.tender.id, data.postData);
+                case 'paste':
+                    return await ctx.service.revisePos.pastePosData(ctx.tender.id, ctx.revise.id, data.postData);
+                default:
+                    throw '未知操作';
+            }
+        }
         async update(ctx) {
             try {
                 if (!ctx.tender.data) throw '标段数据错误';
@@ -387,14 +427,18 @@ module.exports = app => {
                 if (!data.postType || !data.postData) throw '数据错误';
                 const responseData = {err: 0, msg: '', data: {}};
 
+                console.log(data);
                 switch (data.postType) {
                     case 'add':
-                    // case 'delete':
+                    case 'delete':
                     case 'up-move':
                     case 'down-move':
-                    // case 'up-level':
-                    // case 'down-level':
-                        responseData.data = await this._billsBase(ctx, data.postData);
+                    case 'up-level':
+                    case 'down-level':
+                        responseData.data = await this._billsBase(ctx, data.postType, data.postData);
+                        break;
+                    case 'update':
+                        responseData.data = await this.ctx.service.reviseBills.updateCalc(ctx.tender.id, data.postData);
                         break;
                     case 'batch-insert':
                         responseData.data = await this._batchInsert(ctx, data.postData);
@@ -408,11 +452,15 @@ module.exports = app => {
                     case 'paste-block':
                         responseData.data = await this._pasteBlock(ctx, data.postData);
                         break;
+                    case 'pos':
+                        responseData.data = await this._updatePos(ctx, data);
+                        break;
                     default:
                         throw '未知操作';
                 }
                 ctx.body = responseData;
             } catch (err) {
+                console.log(err);
                 this.log(err);
                 ctx.body = this.ajaxErrorBody(err, '数据错误');
             }

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

@@ -966,6 +966,7 @@ const createNewPathTree = function (type, setting) {
          */
         loadPostData(data) {
             const result = {}, reCalcNodes = [];
+            if (!data) return result;
             if (data.delete) {
                 result.delete = this._freeData(data.delete);
                 this._getReCalcNodes(reCalcNodes, result.delete);
@@ -989,6 +990,26 @@ const createNewPathTree = function (type, setting) {
         }
     }
 
+    class ReviseTree extends LedgerTree {
+        checkNodeUsed(node, pos) {
+            if (node.children && node.children.length > 0) {
+                for (const child of node.children) {
+                    const used = this.checkNodeUsed(child, pos);
+                    if (used) return used;
+                }
+            } else {
+                if (node.used) return node.used;
+                const posRange = pos.getLedgerPos(node.id);
+                if (posRange && posRange.length > 0) {
+                    for (const p of posRange) {
+                        if (p.used) return p.used;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
     class StageTree extends FxTree {
         /**
          * 构造函数
@@ -1243,6 +1264,8 @@ const createNewPathTree = function (type, setting) {
         return new StageTree(setting);
     } else if (type === 'ledger') {
         return new LedgerTree(setting);
+    } else if (type = 'revise') {
+        return new ReviseTree(setting);
     } else if (type === 'measure') {
         return new MeasureTree(setting);
     } else if (type === 'master') {

+ 472 - 11
app/public/js/revise.js

@@ -11,6 +11,23 @@
 const ckBillsSpread = window.location.pathname + '-billsSelect';
 
 $(document).ready(() => {
+    toastr.options = {
+        "closeButton": false,
+        "debug": false,
+        "newestOnTop": false,
+        "progressBar": false,
+        "positionClass": "toast-top-center",
+        "preventDuplicates": false,
+        "onclick": null,
+        "showDuration": "300",
+        "hideDuration": "1000",
+        "timeOut": "5000",
+        "extendedTimeOut": "1000",
+        "showEasing": "swing",
+        "hideEasing": "linear",
+        "showMethod": "fadeIn",
+        "hideMethod": "fadeOut"
+    };
     autoFlashHeight();
     // 初始化spread
     const billsSpread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
@@ -55,7 +72,7 @@ $(document).ready(() => {
     treeSetting.calcFun = function (node) {
         node.dgn_price = ZhCalc.round(ZhCalc.div(node.total_price, node.dgn_qty1), 2);
     };
-    const billsTree = createNewPathTree('ledger', treeSetting);
+    const billsTree = createNewPathTree('revise', treeSetting);
     billsTree.loadDatas(billsData);
     treeCalc.calculateAll(billsTree);
     // 加载至spread
@@ -78,7 +95,7 @@ $(document).ready(() => {
                     const rNode = sheet.zh_tree.nodes[sel.row + r];
                     if (rNode.level > node.level) continue;
                     if ((rNode.level < node.level) || (rNode.level === node.level && rNode.pid !== node.pid)) {
-                        toast('请选择同一清单下的节点,进行该操作');
+                        toastr.warning('请选择同一清单下的节点,进行该操作');
                         return;
                     }
                     count += 1;
@@ -145,8 +162,11 @@ $(document).ready(() => {
                 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(tree.nodes.indexOf(d), 1);
+                        sheet.deleteRows(d.deleteIndex, 1);
                     }
                 }
                 // 处理新增
@@ -203,6 +223,45 @@ $(document).ready(() => {
             const [tree, node, count] = this.getDefaultSelectInfo(sheet);
             if (!tree || !node || !count) return;
 
+            if (type === 'delete') {
+                const parent = tree.getParent(node);
+                const children = parent ? parent.children : tree.children;
+                const index = children.indexOf(node);
+                for (let i = 0; i < count; i++) {
+                    const child = children[i+index];
+                    if (tree.checkNodeUsed(child, pos)) {
+                        toastr.warning('选中的清单已计量,不可删除');
+                        return;
+                    }
+                }
+            } else if (type === 'up-level') {
+                const parent = tree.getParent(node);
+                const children = parent ? parent.children : tree.children;
+                const index = children.indexOf(node);
+                for (let i = index; i < children.length; i++) {
+                    const child = children[index];
+                    if (tree.checkNodeUsed(child, pos)) {
+                        if (i >= index + count) {
+                            toastr.warning('其后清单已计量,选中的清单不可升级');
+                        } else {
+                            toastr.warning('选中的清单已计量,不可升级');
+                        }
+                        return;
+                    }
+                }
+            } else if (type === 'down-level') {
+                const parent = tree.getParent(node);
+                const children = parent ? parent.children : tree.children;
+                const index = children.indexOf(node);
+                for (let i = 0; i < count; i++) {
+                    const child = children[i+index];
+                    if (tree.checkNodeUsed(child, pos)) {
+                        toastr.warning('选中的清单已计量,不可降级');
+                        return;
+                    }
+                }
+            }
+
             postData(window.location.pathname + '/update', {
                 postType: type,
                 postData: {
@@ -215,13 +274,18 @@ $(document).ready(() => {
                 if (type === 'delete') {
                     const sel = sheet.getSelections()[0];
                     if (sel) {
-                        sheet.setSelection(tree.nodes.indexOf(node), sel.col, 1, sel.colCount);
+                        sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
                     }
-                } else if (['up-move', 'down-move', 'up-level', 'down-level'].indexOf(type) > -1) {
+                } else if (['up-move', 'down-move'].indexOf(type) > -1) {
                     const sel = sheet.getSelections()[0];
                     if (sel) {
                         sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
                     }
+                } else if (type === 'add') {
+                    const sel = sheet.getSelections()[0];
+                    if (sel) {
+                        sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                    }
                 }
                 self.refreshOperationValid(sheet);
             });
@@ -247,14 +311,14 @@ $(document).ready(() => {
                     return;
                 }
                 // 台账模式,检查部位明细相关
-                if (checkTzMeasureType()) {
+                if (isTz) {
                     if (col.field === 'sgfh_qty' || col.field === 'sgfh_tp' ||
                         col.field === 'sjcl_qty' || col.field === 'sjcl_tp' ||
                         col.field === 'qtcl_qty' || col.field === 'qtcl_tp') {
                         if (!node.children || node.children.length ===0) {
                             const lPos = pos.getLedgerPos(node.id);
                             if (lPos && lPos.length > 0) {
-                                toast('清单含有部位明细,不可修改施工图复核数量', 'error');
+                                toastr.error('清单含有部位明细,不可修改施工图复核数量');
                                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                                 return;
                             }
@@ -263,7 +327,7 @@ $(document).ready(() => {
                     if (col.field === 'b_code' && (info.editingText === '' || !info.editingText)) {
                         const lPos = pos.getLedgerPos(node.id);
                         if (lPos && lPos.length > 0) {
-                            toast('清单含有部位明细,请先删除部位明细,再删除清单编号', 'error');
+                            toastr.error('清单含有部位明细,请先删除部位明细,再删除清单编号');
                             SpreadJsObj.reLoadRowData(info.sheet, info.row);
                             return;
                         }
@@ -282,6 +346,123 @@ $(document).ready(() => {
                 });
             }
         },
+        clipboardPasting: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const range = info.cellRange;
+                for (let iRow = range.row; iRow < range.row + range.rowCount; iRow++) {
+                    const node = info.sheet.zh_tree.nodes[iRow];
+                    if (info.sheet.zh_tree.checkNodeUsed(node, pos)) {
+                        toastr.warning('"' + node.code + node.b_code + ' ' + node.name +'"已计量,请勿修改');
+                        info.cancel = true;
+                        return;
+                    }
+                }
+            }
+        },
+        clipboardPasted: function (e, info) {
+            const tree = info.sheet.zh_tree;
+            if (!tree) { return; }
+
+            const sortData = info.sheet.zh_tree.nodes;
+            const datas = [], filterNodes = [];
+            let bHint = false, bPaste;
+
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                bPaste = false;
+                const curRow = info.cellRange.row + iRow;
+                const node = sortData[curRow];
+                if (node) {
+                    const data = info.sheet.zh_tree.getNodeKeyData(node);
+                    for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                        const curCol = info.cellRange.col + iCol;
+                        const colSetting = info.sheet.zh_setting.cols[curCol];
+                        const value = info.sheet.getText(curRow, curCol).replace('\n', '');
+                        const lPos = pos.getLedgerPos(node.id);
+                        if (lPos && lPos.length > 0) {
+                            if (value === '' && colSetting.field === 'b_code') {
+                                if (!bHint) {
+                                    toastr.warning('清单含有部位明细,请先删除部位明细,再删除清单编号');
+                                    bHint = true;
+                                }
+                                continue;
+                            }
+                            if (colSetting.field === 'sgfh_qty' || colSetting.field === 'sgfh_tp' ||
+                                colSetting.field === 'sjcl_qty' || colSetting.field === 'sjcl_tp' ||
+                                colSetting.field === 'qtcl_qty' || colSetting.field === 'qtcl_tp') {
+                                if (!bHint) {
+                                    toastr.warning('清单含有部位明细,数量金额根据部位明细汇总计算所得,不可编辑');
+                                    bHint = true;
+                                }
+                                continue;
+                            }
+                        }
+                        data[colSetting.field] = value;
+                        bPaste = true;
+                    }
+                    if (bPaste) {
+                        datas.push(data);
+                    } else {
+                        filterNodes.push(node);
+                    }
+                }
+            }
+            if (datas.length > 0) {
+                postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) {
+                        refreshNode.update = refreshNode.update.concat(filterNodes);
+                    }
+                    billsTreeSpreadObj.refreshTree(info.sheet, refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        },
+        deletePress: function (sheet) {
+            if (!sheet.zh_setting) return;
+            const sel = sheet.getSelections()[0], datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                const node = sheet.zh_tree.nodes[iRow];
+                if (sheet.zh_tree.checkNodeUsed(node, pos)) {
+                    toastr.warning('"' + node.code + node.b_code + ' ' + node.name +'"已计量,请勿修改');
+                    return;
+                }
+                const data = sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    const col = sheet.zh_setting.cols[iCol];
+                    data[col.field] = null;
+                }
+                datas.push(data);
+            }
+            if (datas.length > 0) {
+                postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    billsTreeSpreadObj.refreshTree(sheet, refreshNode);
+                });
+            }
+        },
+        pasteBlock: function (spread, copyInfo) {
+            const self = this;
+            const sheet = spread.getActiveSheet();
+            const [tree, node] = this.getDefaultSelectInfo(spread.getActiveSheet());
+
+            postData(window.location.pathname + '/update', {
+                postType: 'paste-block',
+                postData: {
+                    id: tree.getNodeKey(node),
+                    tid: copyInfo.tid,
+                    block: copyInfo.block,
+                }
+            }, function (data) {
+                pos.updateDatas(data.pos);
+                const result = tree.loadPostData(data.ledger);
+                self.refreshTree(sheet, result);
+                self.refreshOperationValid(sheet);
+                removeLocalCache(copyBlockTag);
+            }, null, true);
+        },
         topRowChanged: function (e, info) {
             SpreadJsObj.saveTopAndSelect(info.sheet, ckBillsSpread);
         },
@@ -294,6 +475,10 @@ $(document).ready(() => {
         $('a[name="base-opr"]').click(function () {
             billsTreeSpreadObj.baseOpr(billsSheet, this.getAttribute('type'));
         });
+        billsSpread.bind(spreadNS.Events.EditEnded, billsTreeSpreadObj.editEnded);
+        billsSpread.bind(spreadNS.Events.ClipboardPasting, billsTreeSpreadObj.clipboardPasting);
+        billsSpread.bind(spreadNS.Events.ClipboardPasted, billsTreeSpreadObj.clipboardPasted);
+        SpreadJsObj.addDeleteBind(billsSpread, billsTreeSpreadObj.deletePress);
         let batchInsertObj;
         // 右键菜单
         $.contextMenu({
@@ -328,6 +513,12 @@ $(document).ready(() => {
                         $('#batch').modal('show');
                     }
                 },
+                'debug': {
+                    name: 'debug',
+                    callback: function (key, opt) {
+                        console.log(SpreadJsObj.getSelectObject(billsSheet));
+                    }
+                }
             }
         });
     }
@@ -347,12 +538,275 @@ $(document).ready(() => {
             SpreadJsObj.resetFieldReadOnly(posSheet);
         },
         editStarting: function (e, info) {
-            posSpreadObj.ledgerTreeNode = SpreadJsObj.getSelectObject(billsSheet);
+            posSpreadObj.billsNode = SpreadJsObj.getSelectObject(billsSheet);
+        },
+        /**
+         * 编辑单元格响应事件
+         * @param {Object} e
+         * @param {Object} info
+         */
+        editEnded: function (e, info) {
+            if (!info.sheet.zh_setting) {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            }
+
+            const posData = info.sheet.zh_data ? info.sheet.zh_data[info.row] : null;
+            const col = info.sheet.zh_setting.cols[info.col];
+            const orgText = posData ? posData[col.field] : null;
+            const newText = info.sheet.getCell(info.row, info.col).text();
+            if (orgText === newText || ((!orgText || orgText === '') && (newText === ''))) return;
+
+            const node = posSpreadObj.billsNode;
+            if (!node) {
+                toastr.error('数据错误,请选择台账节点后再试');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            } else if (newText && newText !== '' && node.children && node.children.length > 0) {
+                toastr.error('父节点不可插入部位明细');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            } else if (newText && newText !== '' && (!node.b_code || node.b_code === '')) {
+                toastr.error('项目节不可插入部位明细');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            }
+
+            const data = {postType: 'pos'};
+            if (col.field === 'name') {
+                if (newText === '' && posData) {
+                    toastr.error('部位名称不可为空', 'error');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                } else if (!posData) {
+                    if (newText && newText !== '') {
+                        data.posPostType = 'add';
+                        const sortData = info.sheet.zh_data;
+                        const order = (!sortData || sortData.length === 0) ? 1 : Math.max(sortData[sortData.length - 1].porder + 1, sortData.length + 1);
+                        data.postData = { name: newText, lid: node.id, porder: order};
+                    } else {
+                        return;
+                    }
+                } else {
+                    data.posPostType = 'update-pos';
+                    data.postData = {id: posData.id, name: newText};
+                }
+            } else if (!posData) {
+                toastr.warning('新增部位请先输入名称');
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                return;
+            } else {
+                data.posPostType = 'update';
+                data.postData = {id: posData.id};
+                data.postData[col.field] = col.type === 'Number' ? parseFloat(newText) : newText;
+            }
+            postData(window.location.pathname + '/update', data, function (result) {
+                const updateRst = pos.updateDatas(result.pos);
+                // 刷新当前行, 不适用于新增(在非下一空白行新增)
+                if (updateRst.create.length > 0) {
+                    posSpreadObj.loadCurPosData();
+                } else {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                }
+                const loadResult = billsTree.loadPostData(result.ledger);
+                billsTreeSpreadObj.refreshTree(billsSheet, loadResult);
+                billsTreeSpreadObj.refreshOperationValid(billsSheet);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+            });
+        },
+        /**
+         * 删除按钮响应事件
+         * @param sheet
+         */
+        deletePress: function (sheet) {
+            if (!sheet.zh_settting) return;
+
+            const sortData = sheet.zh_data;
+            const datas = [], posSelects = [];
+            const sel = sheet.getSelections()[0];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                let bDel = false;
+                const node = sortData[iRow];
+                if (node) {
+                    const data = {id: node.id};
+                    for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                        const colSetting = sheet.zh_setting.cols[iCol];
+                        if (colSetting.field === 'name') {
+                            toastr.error('部位名称不能为空');
+                            return;
+                        }
+                        const style = sheet.getStyle(iRow, iCol);
+                        if (!style.locked) {
+                            const colSetting = sheet.zh_setting.cols[iCol];
+                            data[colSetting.field] = null;
+                            bDel = true;
+                        }
+                    }
+                    if (bDel) {
+                        datas.push(data);
+                        posSelects.push(node);
+                    }
+                }
+            }
+            if (datas.length > 0) {
+                postData(window.location.pathname + '/update', {postType: 'pos', posPostType: 'update', postData: datas}, function (result) {
+                    pos.updateDatas(result.pos);
+                    posSpreadObj.loadCurPosData();
+                    const loadResult = billsTree.loadPostData(result.ledger);
+                    billsTreeSpreadObj.refreshTree(billsSheet, loadResult);
+                    billsTreeSpreadObj.refreshOperationValid(billsSheet);
+                }, function () {
+                    posSpreadObj.loadCurPosData();
+                });
+            }
+        },
+        /**
+         * 删除 部位明细
+         * @param sheet
+         */
+        deletePos: function (sheet) {
+            const selection = sheet.getSelections();
+            const data = {
+                postType: 'pos',
+                posPostType: 'delete',
+                postData: [],
+            };
+            const row = selection[0].row, count = selection[0].rowCount;
+            const sortData = sheet.zh_data;
+            for (let iRow = 0; iRow < count; iRow++) {
+                const posData = sortData[iRow + row];
+                if (posData) {
+                    if (posData.used) {
+                        toastr.error('"' + posData.name + '"已计量,请勿删除');
+                        return;
+                    }
+                    data.postData.push(sortData[iRow + row].id);
+                }
+            }
+            console.log(data);
+            if (data.postData.length > 0) {
+                postData(window.location.pathname + '/update', data, function (result) {
+                    pos.removeDatas(result.pos);
+                    sheet.deleteRows(row, count);
+                    const loadResult = billsTree.loadPostData(result.ledger);
+                    billsTreeSpreadObj.refreshTree(billsSheet, loadResult);
+                    billsTreeSpreadObj.refreshOperationValid(billsSheet);
+                });
+            }
+        },
+        clipboardPasting: function (e, info) {
+            if (!info.sheet.zh_setting) {
+                info.cancel = true;
+                return;
+            }
+            const range = info.cellRange;
+            for (let iRow = range.row; iRow < range.row + range.rowCount; iRow++) {
+                const posData = info.sheet.zh_data[iRow];
+                if (posData && posData.used) {
+                    toastr.warning('"' + pos.name +'"已计量,请勿修改');
+                    info.cancel = true;
+                    return;
+                }
+            }
+        },
+        /**
+         * 粘贴单元格响应事件
+         * @param e
+         * @param info
+         */
+        clipboardPasted: function (e, info) {
+            const node = SpreadJsObj.getSelectObject(billsSheet);
+            if (node.code && (node.code !== '')) {
+                toastr.error('项目节不可含有清单明细');
+                posSpreadObj.loadCurPosData();
+                return;
+            }
+            if (node.children && (node.children.length > 0)) {
+                toastr.error('仅清单子项可以含有部位明细');
+                posSpreadObj.loadCurPosData();
+                return;
+            }
+            if (!info.sheet.zh_setting) {
+                posSpreadObj.loadCurPosData();
+                return;
+            }
+
+            const data = [];
+            const sortData = info.sheet.zh_data || [];
+            if (sortData.length === 0 || info.cellRange.row + info.cellRange.rowCount > sortData.length) {
+                if (info.cellRange.col !== 0) {
+                    toast('新增部位请先输入名称', 'warning');
+                    posSpreadObj.loadCurPosData();
+                    return;
+                }
+            }
+            const lastOrder = sortData.length > 0 ? sortData[sortData.length - 1].porder + 1 : 1;
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow++) {
+                const curRow = info.cellRange.row + iRow;
+                const posData = curRow >= sortData.length ? {lid: node.id, porder: lastOrder + curRow - sortData.length} : {id: sortData[curRow].id, lid: node.id};
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    posData[colSetting.field] = info.sheet.getText(curRow, curCol);
+                    if (colSetting.type === 'Number') {
+                        posData[colSetting.field] = _.toNumber(posData[colSetting.field]);
+                    }
+                }
+                data.push(posData);
+            }
+            postData(window.location.pathname + '/update', {postType: 'pos', posPostType: 'paste', postData: data}, function (result) {
+                pos.updateDatas(result.pos);
+                posSpreadObj.loadCurPosData();
+                const loadResult = billsTree.loadPostData(result.ledger);
+                billsTreeSpreadObj.refreshTree(billsSheet, loadResult);
+                posSpreadObj.loadCurPosData();
+                billsTreeSpreadObj.refreshOperationValid(billsSheet);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            });
         },
     };
     posSpreadObj.loadCurPosData();
     SpreadJsObj.resetTopAndSelect(posSheet);
+    if (!readOnly) {
+        posSpread.bind(spreadNS.Events.EditStarting, posSpreadObj.editStarting);
+        posSpread.bind(spreadNS.Events.EditEnded, posSpreadObj.editEnded);
+        posSpread.bind(spreadNS.Events.ClipboardPasting, posSpreadObj.clipboardPasting);
+        posSpread.bind(spreadNS.Events.ClipboardPasted, posSpreadObj.clipboardPasted);
+        SpreadJsObj.addDeleteBind(posSpread, posSpreadObj.deletePress);
+        $.contextMenu({
+            selector: '#pos-spread',
+            build: function ($trigger, e) {
+                const target = SpreadJsObj.safeRightClickSelection($trigger, e, posSpread);
+                return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+            },
+            items: {
+                'delete': {
+                    name: '删除',
+                    icon: 'fa-remove',
+                    disabled: function (key, opt) {
+                        if (posSheet.zh_data) {
+                            const selection = posSheet.getSelections();
+                            return posSheet.zh_data.length < selection[0].row + selection[0].rowCount;
+                        } else {
+                            return true;
+                        }
+                    },
+                    callback: function (key, opt) {
+                        posSpreadObj.deletePos(posSheet);
+                    }
+                },
+                'debug': {
+                    name: 'debug',
+                    callback: function (key, opt) {
+                        console.log(SpreadJsObj.getSelectObject(posSheet));
+                    }
+                }
+            }
 
+        });
+    }
     $.divResizer({
         select: '#revise-resize',
         callback: function () {
@@ -387,7 +841,7 @@ $(document).ready(() => {
 
                 if (stdType === 'bills') {
                     if (!(mainNode.b_code && mainNode.b_code !== '') && !mainTree.isLeafXmj(mainNode)) {
-                        toast('非最底层项目下,不应添加清单', 'warning');
+                        toastr.warning('非最底层项目下,不应添加清单');
                         return;
                     }
                 }
@@ -404,6 +858,9 @@ $(document).ready(() => {
                 }, function (result) {
                     const refreshNode = mainTree.loadPostData(result);
                     billsTreeSpreadObj.refreshTree(mainSheet, refreshNode);
+                    if (sel && refreshNode.create[0]) {
+                        mainSheet.setSelection(mainTree.nodes.indexOf(refreshNode.create[0]), sel.col, sel.rowCount, sel.colCount);
+                    }
                     billsTreeSpreadObj.refreshOperationValid(mainSheet);
                 });
             });
@@ -450,7 +907,7 @@ $(document).ready(() => {
                 if (!mainNode || !mainTree) { return; }
 
                 if (mainNode.code && mainNode.code !== '' && !mainTree.isLeafXmj(mainNode)) {
-                    toast('非最底层项目下,不应添加清单', 'error');
+                    toastr.warning('非最底层项目下,不应添加清单');
                     return;
                 }
 
@@ -467,6 +924,10 @@ $(document).ready(() => {
                 }, function (result) {
                     const refreshData = mainTree.loadPostData(result);
                     billsTreeSpreadObj.refreshTree(mainSheet, refreshData);
+                    const sel = mainSheet.getSelections()[0];
+                    if (sel && refreshData.create[0]) {
+                        mainSheet.setSelection(mainTree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                    }
                     billsTreeSpreadObj.refreshOperationValid(mainSheet);
                 });
             });

+ 109 - 109
app/service/ledger.js

@@ -279,17 +279,17 @@ module.exports = app => {
          * @return {Object<{id: *}>}
          * @private
          */
-        _filterUpdateInvalidField(id, data) {
-            const result = {
-                id,
-            };
-            for (const prop in data) {
-                if (readOnlyFields.indexOf(prop) === -1) {
-                    result[prop] = data[prop];
-                }
-            }
-            return result;
-        }
+        // _filterUpdateInvalidField(id, data) {
+        //     const result = {
+        //         id,
+        //     };
+        //     for (const prop in data) {
+        //         if (readOnlyFields.indexOf(prop) === -1) {
+        //             result[prop] = data[prop];
+        //         }
+        //     }
+        //     return result;
+        // }
 
         /**
          * newData中,以orgData为基准,过滤掉orgData中未定义或值相等的部分
@@ -325,14 +325,14 @@ module.exports = app => {
          * @return {boolean}
          * @private
          */
-        _checkCalcField(data) {
-            for (const prop in data) {
-                if (calcFields.indexOf(prop) >= 0) {
-                    return true;
-                }
-            }
-            return false;
-        }
+        // _checkCalcField(data) {
+        //     for (const prop in data) {
+        //         if (calcFields.indexOf(prop) >= 0) {
+        //             return true;
+        //         }
+        //     }
+        //     return false;
+        // }
 
         /**
          * 复制粘贴整块
@@ -446,96 +446,96 @@ module.exports = app => {
          * @param {Object} data
          * @return {Promise<*>}
          */
-        async updateCalc(tenderId, data) {
-            // 简单验证数据
-            if (tenderId <= 0 || !this.ctx.tender) {
-                throw '标段不存在';
-            }
-            const info = this.ctx.tender.info;
-            if (!data) {
-                throw '提交数据错误';
-            }
-            const datas = data instanceof Array ? data : [data];
-            const ids = [];
-            for (const row of datas) {
-                if (tenderId !== row.tender_id) {
-                    throw '提交数据错误';
-                }
-                ids.push(row.id);
-            }
-
-            this.transaction = await this.db.beginTransaction();
-            try {
-                for (const row of datas) {
-                    const updateNode = await this.getDataById(row.id);
-                    if (!updateNode || tenderId !== updateNode.tender_id || row.ledger_id !== updateNode.ledger_id) {
-                        throw '提交数据错误';
-                    }
-                    let updateData;
-                    if (row.unit) {
-                        if (row.sgfh_qty === undefined) { row.sgfh_qty = updateNode.sgfh_qty; }
-                        if (row.sjcl_qty === undefined) { row.sjcl_qty = updateNode.sjcl_qty; }
-                        if (row.qtcl_qty === undefined) { row.qtcl_qty = updateNode.qtcl_qty; }
-                        if (row.deal_qty === undefined) { row.deal_qty = updateNode.deal_qty; }
-                    }
-                    if (row.b_code) {
-                        row.dgn_qty1 = null;
-                        row.dgn_qty2 = null;
-                    }
-                    if (this._checkCalcField(row)) {
-                        let calcData = JSON.parse(JSON.stringify(row));
-                        const precision = this.ctx.helper.findPrecision(info.precision, row.unit ? row.unit : updateNode.unit);
-                        // 数量保留小数位数
-                        this.ctx.helper.checkFieldPrecision(calcData, qtyFields, precision.value);
-                        // 单位保留小数位数
-                        this.ctx.helper.checkFieldPrecision(calcData, upFields, info.decimal.up);
-                        // 未提交单价则读取数据库单价
-                        if (row.unit_price === undefined) calcData.unit_price = updateNode.unit_price;
-                        // 计算
-                        if (row.sgfh_qty !== undefined || row.sjcl_qty !== undefined || row.qtcl_qty !== undefined ||
-                            row.deal_qty !== undefined || row.unit_price) {
-                            if (row.sgfh_qty === undefined) calcData.sgfh_qty = updateNode.sgfh_qty;
-                            if (row.sjcl_qty === undefined) calcData.sjcl_qty = updateNode.sjcl_qty;
-                            if (row.qtcl_qty === undefined) calcData.qtcl_qty = updateNode.qtcl_qty;
-                            if (row.deal_qty === undefined) calcData.deal_qty = updateNode.deal_qty;
-                            calcData.quantity = this.ctx.helper.sum([calcData.sgfh_qty, calcData.sjcl_qty, calcData.qtcl_qty]);
-                            calcData.sgfh_tp = this.ctx.helper.mul(calcData.sgfh_qty, calcData.unit_price, info.decimal.tp);
-                            calcData.sjcl_tp = this.ctx.helper.mul(calcData.sjcl_qty, calcData.unit_price, info.decimal.tp);
-                            calcData.qtcl_tp = this.ctx.helper.mul(calcData.qtcl_qty, calcData.unit_price, info.decimal.tp);
-                            calcData.total_price = this.ctx.helper.mul(calcData.quantity, calcData.unit_price, info.decimal.tp);
-                            calcData.deal_tp = this.ctx.helper.mul(calcData.deal_qty, calcData.unit_price, info.decimal.tp);
-                        } else if (row.sgfh_tp !== undefined || row.sjcl_tp !== undefined || row.qtcl_tp !== undefined || row.deal_tp !== undefined) {
-                            calcData.sgfh_qty = null;
-                            calcData.sjcl_qty = null;
-                            calcData.qtcl_qty = null;
-                            calcData.quantity = null;
-                            calcData.deal_qty = null;
-                            calcData.sgfh_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.sgfh_tp, info.decimal.tp) : updateNode.sgfh_tp;
-                            calcData.sjcl_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.sjcl_tp, info.decimal.tp) : updateNode.sjcl_tp;
-                            calcData.qtcl_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.qtcl_tp, info.decimal.tp) : updateNode.qtcl_tp;
-                            calcData.total_price = this.ctx.helper.sum([calcData.sgfh_tp, calcData.sjcl_tp, calcData.qtcl_tp]);
-                            calcData.deal_tp = (row.deal_tp !== undefined) ? this.ctx.helper.round(calcData.row.deal_tp, info.decimal.tp) : updateNode.deal_tp;
-                        } else if (row.unit_price !== undefined) {
-                            calcData.sgfh_tp = this.ctx.helper.mul(calcData.sgfh_qty, calcData.unit_price, info.decimal.tp);
-                            calcData.sjcl_tp = this.ctx.helper.mul(calcData.sjcl_qty, calcData.unit_price, info.decimal.tp);
-                            calcData.qtcl_tp = this.ctx.helper.mul(calcData.qtcl_qty, calcData.unit_price, info.decimal.tp);
-                            calcData.total_price = this.ctx.helper.mul(calcData.quantity, calcData.unit_price, info.decimal.tp);
-                            calcData.deal_tp = this.ctx.helper.mul(calcData.deal_qty, calcData.unit_price, info.decimal.tp);
-                        }
-                        updateData = this._filterUpdateInvalidField(updateNode.id, calcData);
-                    } else {
-                        updateData = this._filterUpdateInvalidField(updateNode.id, row);
-                    }
-                    await this.transaction.update(this.tableName, updateData);
-                }
-                await this.transaction.commit();
-            } catch (err) {
-                await this.transaction.rollback();
-                throw err;
-            }
-
-            return { update: await this.getDataByIds(ids) };
-        }
+        // async updateCalc(tenderId, data) {
+        //     // 简单验证数据
+        //     if (tenderId <= 0 || !this.ctx.tender) {
+        //         throw '标段不存在';
+        //     }
+        //     const info = this.ctx.tender.info;
+        //     if (!data) {
+        //         throw '提交数据错误';
+        //     }
+        //     const datas = data instanceof Array ? data : [data];
+        //     const ids = [];
+        //     for (const row of datas) {
+        //         if (tenderId !== row.tender_id) {
+        //             throw '提交数据错误';
+        //         }
+        //         ids.push(row.id);
+        //     }
+        //
+        //     this.transaction = await this.db.beginTransaction();
+        //     try {
+        //         for (const row of datas) {
+        //             const updateNode = await this.getDataById(row.id);
+        //             if (!updateNode || tenderId !== updateNode.tender_id || row.ledger_id !== updateNode.ledger_id) {
+        //                 throw '提交数据错误';
+        //             }
+        //             let updateData;
+        //             if (row.unit) {
+        //                 if (row.sgfh_qty === undefined) { row.sgfh_qty = updateNode.sgfh_qty; }
+        //                 if (row.sjcl_qty === undefined) { row.sjcl_qty = updateNode.sjcl_qty; }
+        //                 if (row.qtcl_qty === undefined) { row.qtcl_qty = updateNode.qtcl_qty; }
+        //                 if (row.deal_qty === undefined) { row.deal_qty = updateNode.deal_qty; }
+        //             }
+        //             if (row.b_code) {
+        //                 row.dgn_qty1 = null;
+        //                 row.dgn_qty2 = null;
+        //             }
+        //             if (this._checkCalcField(row)) {
+        //                 let calcData = JSON.parse(JSON.stringify(row));
+        //                 const precision = this.ctx.helper.findPrecision(info.precision, row.unit ? row.unit : updateNode.unit);
+        //                 // 数量保留小数位数
+        //                 this.ctx.helper.checkFieldPrecision(calcData, qtyFields, precision.value);
+        //                 // 单位保留小数位数
+        //                 this.ctx.helper.checkFieldPrecision(calcData, upFields, info.decimal.up);
+        //                 // 未提交单价则读取数据库单价
+        //                 if (row.unit_price === undefined) calcData.unit_price = updateNode.unit_price;
+        //                 // 计算
+        //                 if (row.sgfh_qty !== undefined || row.sjcl_qty !== undefined || row.qtcl_qty !== undefined ||
+        //                     row.deal_qty !== undefined || row.unit_price) {
+        //                     if (row.sgfh_qty === undefined) calcData.sgfh_qty = updateNode.sgfh_qty;
+        //                     if (row.sjcl_qty === undefined) calcData.sjcl_qty = updateNode.sjcl_qty;
+        //                     if (row.qtcl_qty === undefined) calcData.qtcl_qty = updateNode.qtcl_qty;
+        //                     if (row.deal_qty === undefined) calcData.deal_qty = updateNode.deal_qty;
+        //                     calcData.quantity = this.ctx.helper.sum([calcData.sgfh_qty, calcData.sjcl_qty, calcData.qtcl_qty]);
+        //                     calcData.sgfh_tp = this.ctx.helper.mul(calcData.sgfh_qty, calcData.unit_price, info.decimal.tp);
+        //                     calcData.sjcl_tp = this.ctx.helper.mul(calcData.sjcl_qty, calcData.unit_price, info.decimal.tp);
+        //                     calcData.qtcl_tp = this.ctx.helper.mul(calcData.qtcl_qty, calcData.unit_price, info.decimal.tp);
+        //                     calcData.total_price = this.ctx.helper.mul(calcData.quantity, calcData.unit_price, info.decimal.tp);
+        //                     calcData.deal_tp = this.ctx.helper.mul(calcData.deal_qty, calcData.unit_price, info.decimal.tp);
+        //                 } else if (row.sgfh_tp !== undefined || row.sjcl_tp !== undefined || row.qtcl_tp !== undefined || row.deal_tp !== undefined) {
+        //                     calcData.sgfh_qty = null;
+        //                     calcData.sjcl_qty = null;
+        //                     calcData.qtcl_qty = null;
+        //                     calcData.quantity = null;
+        //                     calcData.deal_qty = null;
+        //                     calcData.sgfh_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.sgfh_tp, info.decimal.tp) : updateNode.sgfh_tp;
+        //                     calcData.sjcl_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.sjcl_tp, info.decimal.tp) : updateNode.sjcl_tp;
+        //                     calcData.qtcl_tp = (row.sgfh_tp !== undefined) ? this.ctx.helper.round(calcData.row.qtcl_tp, info.decimal.tp) : updateNode.qtcl_tp;
+        //                     calcData.total_price = this.ctx.helper.sum([calcData.sgfh_tp, calcData.sjcl_tp, calcData.qtcl_tp]);
+        //                     calcData.deal_tp = (row.deal_tp !== undefined) ? this.ctx.helper.round(calcData.row.deal_tp, info.decimal.tp) : updateNode.deal_tp;
+        //                 } else if (row.unit_price !== undefined) {
+        //                     calcData.sgfh_tp = this.ctx.helper.mul(calcData.sgfh_qty, calcData.unit_price, info.decimal.tp);
+        //                     calcData.sjcl_tp = this.ctx.helper.mul(calcData.sjcl_qty, calcData.unit_price, info.decimal.tp);
+        //                     calcData.qtcl_tp = this.ctx.helper.mul(calcData.qtcl_qty, calcData.unit_price, info.decimal.tp);
+        //                     calcData.total_price = this.ctx.helper.mul(calcData.quantity, calcData.unit_price, info.decimal.tp);
+        //                     calcData.deal_tp = this.ctx.helper.mul(calcData.deal_qty, calcData.unit_price, info.decimal.tp);
+        //                 }
+        //                 updateData = this._filterUpdateInvalidField(updateNode.id, calcData);
+        //             } else {
+        //                 updateData = this._filterUpdateInvalidField(updateNode.id, row);
+        //             }
+        //             await this.transaction.update(this.tableName, updateData);
+        //         }
+        //         await this.transaction.commit();
+        //     } catch (err) {
+        //         await this.transaction.rollback();
+        //         throw err;
+        //     }
+        //
+        //     return { update: await this.getDataByIds(ids) };
+        // }
 
         /**
          *

+ 11 - 4
app/service/ledger_revise.js

@@ -78,10 +78,13 @@ module.exports = app => {
 
         async _initReviseBills(transaction, tid) {
             const sql = 'Insert Into ' + this.ctx.service.reviseBills.tableName +
-                '  (id, code, b_code, name, unit, source, remark, ledger_id, ledger_pid, level, order, full_path, is_leaf,' +
+                '  (id, code, b_code, name, unit, source, remark, ledger_id, ledger_pid, level, `order`, full_path, is_leaf,' +
                 '     quantity, total_price, unit_price, drawing_code, memo, dgn_qty1, dgn_qty2, deal_qty, deal_tp,' +
-                '     sgfh_qty, sgfh_tp, sjcl_qty, sjcl_tp, qtcl_qty, qtcl_tp, node_type, crid)' +
-                '  Select * From ' + this.ctx.service.ledger.tableName +
+                '     sgfh_qty, sgfh_tp, sjcl_qty, sjcl_tp, qtcl_qty, qtcl_tp, node_type, crid, tender_id)' +
+                '  Select id, code, b_code, name, unit, source, remark, ledger_id, ledger_pid, level, `order`, full_path, is_leaf,' +
+                '      quantity, total_price, unit_price, drawing_code, memo, dgn_qty1, dgn_qty2, deal_qty, deal_tp,' +
+                '      sgfh_qty, sgfh_tp, sjcl_qty, sjcl_tp, qtcl_qty, qtcl_tp, node_type, crid, tender_id' +
+                '  From ' + this.ctx.service.ledger.tableName +
                 '  Where `tender_id` = ?';
             const sqlParam = [tid];
             await transaction.query(sql, sqlParam);
@@ -89,7 +92,11 @@ module.exports = app => {
 
         async _initRevisePos(transaction, tid) {
             const sql = 'Insert Into ' + this.ctx.service.revisePos.tableName +
-                '  Select * From ' + this.ctx.service.pos.tableName +
+                '  (id, tid, lid, name, drawing_code, quantity, add_stage, add_times, add_user,' +
+                '     sgfh_qty, sjcl_qty, qtcl_qty, crid)' +
+                '  Select id, tid, lid, name, drawing_code, quantity, add_stage, add_times, add_user,' +
+                '     sgfh_qty, sjcl_qty, qtcl_qty, crid' +
+                '  From ' + this.ctx.service.pos.tableName +
                 '  Where `tid` = ?';
             const sqlParam = [tid];
             await transaction.query(sql, sqlParam);

+ 40 - 0
app/service/revise_bills.js

@@ -228,6 +228,46 @@ module.exports = app => {
             result.pos = await this.ctx.service.revisePos.getDataByLid(tid, newIds);
             return result;
         }
+
+        /**
+         *
+         * @param node
+         * @param transaction
+         * @returns {Promise<void>}
+         * @private
+         */
+        async _calcNode(node, transaction) {
+            const info = this.ctx.tender.info;
+            const precision = this.ctx.helper.findPrecision(info.precision, node.unit);
+
+            const calcQtySql = 'SELECT SUM(`sgfh_qty`) As `sgfh_qty`, SUM(`sjcl_qty`) As `sjcl_qty`, SUM(`qtcl_qty`) As `qtcl_qty`, SUM(`quantity`) As `quantity` FROM ?? WHERE `lid` = ?';
+            const data = await transaction.queryOne(calcQtySql, [this.ctx.service.revisePos.tableName, node.id]);
+            data.id = node.id;
+            data.sgfh_qty = this.round(data.sgfh_qty, precision.value);
+            data.sjcl_qty = this.round(data.sjcl_qty, precision.value);
+            data.qtcl_qty = this.round(data.qtcl_qty, precision.value);
+            data.quantity = this.round(data.quantity, precision.value);
+            data.sgfh_tp = this.ctx.helper.mul(data.sgfh_qty, node.unit_price, info.decimal.tp);
+            data.sjcl_tp = this.ctx.helper.mul(data.sjcl_qty, node.unit_price, info.decimal.tp);
+            data.qtcl_tp = this.ctx.helper.mul(data.qtcl_qty, node.unit_price, info.decimal.tp);
+            data.total_price = this.ctx.helper.mul(data.quantity, node.unit_price, info.decimal.tp);
+            const result = await transaction.update(this.tableName, data);
+        }
+
+        /**
+         *
+         * @param {Number} tid - 标段id
+         * @param {Number} id - 需要计算的节点的id
+         * @param {Object} transaction - 操作所属事务,没有则创建
+         * @return {Promise<void>}
+         */
+        async calc(tid, id, transaction) {
+            const node = await transaction.get(this.tableName, {id: id});
+            if (!node) {
+                throw '数据错误';
+            }
+            await this._calcNode(node, transaction);
+        }
     }
 
     return ReviseBills;

+ 257 - 0
app/service/revise_pos.js

@@ -22,6 +22,15 @@ module.exports = app => {
             this.tableName = 'revise_pos';
         }
 
+        async getPosData(condition) {
+            const sql = 'SELECT p.id, p.tid, p.lid, p.name, p.quantity, p.drawing_code, p.sgfh_qty, p.sjcl_qty, p.qtcl_qty, p.porder, p.add_stage, p.add_times, p.add_user, s.order As add_stage_order ' +
+                '  FROM ' + this.tableName + ' p ' +
+                '  LEFT JOIN ' + this.ctx.service.stage.tableName + ' s' +
+                '  ON add_stage = s.id'
+                + this.ctx.helper.whereSql(condition, 'p');
+            return await this.db.query(sql);
+        }
+
         /**
          * 获取 修订 清单数据
          * @param {Number}tid - 标段id
@@ -57,6 +66,254 @@ module.exports = app => {
             }
             await transaction.insert(this.tableName, insertDatas);
         }
+
+        /**
+         * 删除清单下部位明细数据(删除清单时调用)
+         *
+         * @param transaction - 事务
+         * @param tid - 标段id
+         * @param lid - 清单id
+         * @returns {Promise<void>}
+         */
+        async deletePosData(transaction, tid, lid) {
+            await transaction.delete(this.tableName, {tid: tid, lid: lid});
+        }
+
+        async _insertPosData(transaction, data, tid, rid) {
+            data.id = this.uuid.v4();
+            data.tid = tid;
+            // todo 新增期
+            data.add_stage = 0;
+            data.add_times = 0;
+            data.in_time = new Date();
+            data.add_user = this.ctx.session.sessionUser.accountId;
+            data.crid = rid;
+            if (data.quantity) {
+                const bills = await this.ctx.service.reviseBills.getDataById(data.lid);
+                const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, bills.unit);
+                data.quantity = this.round(data.quantity, precision.value);
+            }
+            const addRst = await transaction.insert(this.tableName, data);
+        }
+
+        async addPos(tid, rid, data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (data instanceof Array) {
+                    for (const d of data) {
+                        this._insertPosData(transaction, d, tid, rid);
+                    }
+                } else {
+                    this._insertPosData(transaction, data, tid, rid);
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return {pos: data};
+        }
+
+        async updatePos(tid, data) {
+            const billsIds = [];
+            const datas = data instanceof Array ? data : [data];
+            const orgPos = await this.getPosData({tid: tid, id: this._.map(datas, 'id')});
+            const transaction = await this.db.beginTransaction();
+            try {
+                for (const d of datas) {
+                    const op = this._.find(orgPos, function (p) { return p.id = d.id; });
+                    if (d.sgfh_qty !== undefined || d.qtcl_qty !== undefined || d.sjcl_qty !== undefined) {
+                        const bills = await this.ctx.service.reviseBills.getDataById(op.lid);
+                        const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, bills.unit);
+                        if (d.sgfh_qty !== undefined) {
+                            d.sgfh_qty = this.round(d.sgfh_qty, precision.value);
+                        } else if (op) {
+                            d.sgfh_qty = op.sgfh_qty;
+                        }
+                        if (d.sjcl_qty !== undefined) {
+                            d.sjcl_qty = this.round(d.sjcl_qty, precision.value);
+                        } else if (op) {
+                            d.sjcl_qty = op.sjcl_qty;
+                        }
+                        if (d.qtcl_qty !== undefined) {
+                            d.qtcl_qty = this.round(d.qtcl_qty, precision.value);
+                        } else if (op) {
+                            d.qtcl_qty = op.qtcl_qty;
+                        }
+                        d.quantity = this.ctx.helper.sum([d.sgfh_qty, d.qtcl_qty, d.sjcl_qty]);
+                    }
+                    await transaction.update(this.tableName, d, {tid: tid, id: d.id});
+                    if (d.quantity !== undefined && op && (billsIds.indexOf(op.lid) === -1)) {
+                        billsIds.push(op.lid);
+                    }
+                }
+                for (const lid of billsIds) {
+                    await this.ctx.service.reviseBills.calc(tid, lid, transaction);
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            const bills = await this.ctx.service.reviseBills.getDataById(billsIds);
+            return {pos: data, ledger: {update: bills}};
+        }
+
+        async deletePos(tid, data) {
+            if (!data || data.length === 0) {
+                throw '提交数据错误';
+            }
+            const pos = await this.getPosData({tid: tid, id: data});
+            const ledgerIds = this._.map(pos, 'lid');
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, {tid: tid, id: data});
+                for (const lid of ledgerIds) {
+                    await this.ctx.service.reviseBills.calc(tid, lid, transaction);
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            const bills = await this.ctx.service.reviseBills.getDataById(ledgerIds);
+            return {ledger: {update: bills}, pos: data};
+        }
+
+        /**
+         * 保存部位明细数据
+         * @param data
+         * @param {Number} tid - 标段id
+         * @returns {Promise<{ledger: {}, pos: null}>}
+         */
+        async savePosData(tid, rid, data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (data.posPostType === 'add') {
+                    if (data.updateData instanceof Array) {
+                        for (const d of data.postData) {
+                            this._insertPosData(transaction, d, tid, rid);
+                        }
+                    } else {
+                        this._insertPosData(transaction, data.postData, tid, rid);
+                    }
+                } else if (data.posPostType === 'update') {
+                    const datas = data.postData instanceof Array ? data.postData : [data.postData];
+                    result.ledger.update = [];
+                    const orgPos = await this.getPosData({tid: tid, id: this._.map(datas, 'id')});
+                    for (const d of datas) {
+                        const op = this._.find(orgPos, function (p) { return p.id = d.id; });
+                        if (d.sgfh_qty !== undefined || d.qtcl_qty !== undefined || d.sjcl_qty !== undefined) {
+                            const bills = await this.ctx.service.reviseBills.getDataById(op.lid);
+                            const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, bills.unit);
+                            if (d.sgfh_qty !== undefined) {
+                                d.sgfh_qty = this.round(d.sgfh_qty, precision.value);
+                            } else if (op) {
+                                d.sgfh_qty = op.sgfh_qty;
+                            }
+                            if (d.sjcl_qty !== undefined) {
+                                d.sjcl_qty = this.round(d.sjcl_qty, precision.value);
+                            } else if (op) {
+                                d.sjcl_qty = op.sjcl_qty;
+                            }
+                            if (d.qtcl_qty !== undefined) {
+                                d.qtcl_qty = this.round(d.qtcl_qty, precision.value);
+                            } else if (op) {
+                                d.qtcl_qty = op.qtcl_qty;
+                            }
+                            d.quantity = this.ctx.helper.sum([d.sgfh_qty, d.qtcl_qty, d.sjcl_qty]);
+                        }
+                        await transaction.update(this.tableName, d, {tid: tid, id: d.id});
+                        if (d.quantity !== undefined && op && (result.ledger.update.indexOf(op.lid) === -1)) {
+                            result.ledger.update.push(op.lid);
+                        }
+                    }
+                    for (const lid of result.ledger.update) {
+                        await this.ctx.service.reviseBills.calc(tid, lid, transaction);
+                    }
+                } else if (data.posPostType === 'delete') {
+                    if (!data.postData || data.postData.length === 0) {
+                        throw '提交数据错误';
+                    }
+                    const pos = await this.getPosData({tid: tid, id: data.postData});
+                    const ledgerIds = this._.map(pos, 'lid');
+                    await transaction.delete(this.tableName, {tid: tid, id: data.postData});
+                    for (const lid of ledgerIds) {
+                        await this.ctx.service.reviseBills.calc(tid, lid, transaction);
+                    }
+                    result.ledger.update = ledgerIds;
+                } else {
+                    throw '提交数据错误';
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            result.pos = data.postData;
+            result.ledger.update = await this.ctx.service.reviseBills.getDataById(result.ledger.update);
+            return result;
+        }
+
+        /**
+         * 复制粘贴 部位明细数据
+         * @param {Array} data - 复制粘贴的数据
+         * @param {Number} tid - 标段id
+         * @returns {Promise<{ledger: {}, pos: null}>}
+         */
+        async pastePosData(tid, rid, data) {
+            if (!(data instanceof Array)) throw '提交数据错误';
+
+            const transaction = await this.db.beginTransaction();
+            const result = { ledger: {}, pos: null }, updateLid = [];
+            const orgPos = await this.getPosData({tid: tid, id: this._.map(data, 'id')});
+            let bills = null, precision = null;
+            try {
+                for (const d of data) {
+                    const op = d.id ? this._.find(orgPos, {id: d.id}) : null;
+                    if (d.sgfh_qty || d.sjcl_qty || d.qtcl_qty) {
+                        if (!bills || bills.id !== d.lid) {
+                            bills = await this.ctx.service.reviseBills.getDataById(d.lid);
+                            precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, bills.unit);
+                            updateLid.push(d.lid);
+                        }
+                        if (d.sgfh_qty !== undefined) {
+                            d.sgfh_qty = this.round(d.sgfh_qty, precision.value);
+                        } else if (op) {
+                            d.sgfh_qty = op.sgfh_qty;
+                        }
+                        if (d.sjcl_qty !== undefined) {
+                            d.sjcl_qty = this.round(d.sjcl_qty, precision.value);
+                        } else if (op) {
+                            d.sjcl_qty = op.sjcl_qty;
+                        }
+                        if (d.qtcl_qty) {
+                            d.qtcl_qty = this.round(d.qtcl_qty, precision.value);
+                        } else if (op) {
+                            d.qtcl_qty = op.qtcl_qty;
+                        }
+                        d.quantity = this.ctx.helper.sum([d.sgfh_qty, d.qtcl_qty, d.sjcl_qty]);
+                    }
+                    if (d.id) {
+                        await transaction.update(this.tableName, d);
+                    } else {
+                        this._insertPosData(transaction, d, tid, rid);
+                    }
+                }
+                for (const lid of updateLid) {
+                    await this.ctx.service.reviseBills.calc(tid, lid, transaction);
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            result.pos = data;
+            if (updateLid.length > 0) {
+                result.ledger.update = await this.ctx.service.reviseBills.getDataById(updateLid);
+            }
+            return result;
+        }
     }
 
     return RevisePos;

+ 13 - 0
app/service/stage_bills.js

@@ -114,6 +114,19 @@ module.exports = app => {
             }
         }
 
+        async getUsedStageBills(tid, sid) {
+            const sql = 'SELECT Bills.lid, (Bills.contract_qty <> 0 and Bills.qc_qty <> 0) As used FROM ' + this.tableName + ' As Bills ' +
+                '  INNER JOIN ( ' +
+                '    SELECT MAX(`times` * ' + timesLen + ' + `order`) As `progress`, `lid`, `sid` From ' + this.tableName +
+                '      WHERE tid = ? And sid = ?' +
+                '      GROUP BY `lid`' +
+                '  ) As MaxFilter ' +
+                '  ON (Bills.times * ' + timesLen + ' + `order`) = MaxFilter.progress And Bills.lid = MaxFilter.lid And Bills.`sid` = MaxFilter.`sid`';
+            const sqlParam = [tid, sid];
+            const stageBills = await this.db.query(sql, sqlParam);
+            return this._.map(this._.filter(stageBills, 'used'), pid);
+        }
+
         /**
          * 获取截止本期数据
          * @param {Number} tid - 标段id

+ 8 - 0
app/service/stage_bills_final.js

@@ -64,6 +64,14 @@ module.exports = app => {
             return await this.db.query(sql, sqlParam);
         }
 
+        async getUsedBills(tid, sorder) {
+            const usedBills = await this.getAllDataByCondition({
+                columns: ['lid'],
+                where: {tid: tid, sorder: sorder, used: true}
+            });
+            return this._.map(usedBills, 'lid');
+        }
+
         /**
          * 生成本期最终数据
          * @param transaction - 所属事务

+ 12 - 0
app/service/stage_pos.js

@@ -124,6 +124,18 @@ module.exports = app => {
             return this._filterLastestData(stagePos);
         }
 
+        async getStageUsedPos(tid, sid, where) {
+            const stagePos = await this.getLastestStageData2(tid, sid, where);
+            const pids = this._.map(stagePos, function (sp) {
+                if (this.ctx.helper.checkZero(sp.contract_qty) && this.ctx.helper.checkZero(sp.qc_qty)) {
+                    return sp.pid;
+                } else {
+                    return -1;
+                }
+            });
+            return this._.full(pids, -1);
+        }
+
         /**
          * 新增部位明细数据(仅供updateStageData调用)
          *

+ 8 - 0
app/service/stage_pos_final.js

@@ -48,6 +48,14 @@ module.exports = app => {
             });
         }
 
+        async getUsedPos(tid, sorder) {
+            const usedBills = await this.getAllDataByCondition({
+                columns: ['pid'],
+                where: {tid: tid, sorder: sorder, used: true}
+            });
+            return this._.map(usedBills, 'pid');
+        }
+
         /**
          * 生成本期最终数据
          * @param transaction - 所属事务

+ 2 - 2
app/view/revise/info.ejs

@@ -25,8 +25,8 @@
                 <div class="d-inline-block">
                     <a href="javascript: void(0);" name="base-opr" type="add" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="插入"><i class="fa fa-plus" aria-hidden="true"></i></a>
                     <a href="javascript: void(0);" name="base-opr" type="delete" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
-                    <a href="javascript: void(0);" name="base-opr" type="up-level" class="btn btn-sm disabled" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
-                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm disabled" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-level" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
                     <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
                     <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
                     <a href="javascript: void(0);" id="copy" class="btn btn-sm" data-toggle="tooltip disabled" data-placement="bottom" title="" data-original-title="复制"><i class="fa fa-files-o" aria-hidden="true"></i></a>

+ 1 - 0
config/web.js

@@ -132,6 +132,7 @@ const JsFiles = {
                 files: [
                     "/public/js/spreadjs/sheets/gc.spread.sheets.all.10.0.1.min.js",
                     "/public/js/decimal.min.js",
+                    "/public/js/toastr.min.js",
                 ],
                 mergeFiles: [
                     "/public/js/sub_menu.js",